Compare commits

..
Author SHA1 Message Date
Niels Lohmann 414b4e6d27 Merge develop into fix/bjdata-count-size-max
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 18:27:15 +01:00
Niels Lohmann da6d21ae24 MessagePack reader: reject an array/map 32 length equal to SIZE_MAX
On a platform with a 32-bit std::size_t, an array 32 or map 32 length of
0xFFFFFFFF equals unknown_size(), so start_array()/start_object() were
told the size was unknown and skipped the max_size() check. Route both
through the CBOR length check, renamed get_container_size(), which
already rejects that value.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 18:25:06 +01:00
Niels Lohmann db73a6044f UBJSON/BJData reader: reject a container count equal to SIZE_MAX
The reader marks a container whose count is not known by npos, which
is SIZE_MAX. A count of exactly that value was stored unchanged and
then read as "no count", so the BJData input

    [ # M ff ff ff ff ff ff ff ff i 01 ]

parsed as [1] instead of throwing out_of_range.408 as a count of
2^64-2 does. Where size_t is 32 bits, the same holds for 'm' and 'L'
counts of 2^32-1, in UBJSON too.

The counts of 'm', 'M' and the signed types now go through
store_ubjson_count(), which rejects npos with "integer value overflow"
(the message a count beyond size_t already gets), as the CBOR reader
does in get_cbor_container_size(). An ND-array dimension of 2^64-1 gets
the same message now, where it was "excessive ndarray size caused
overflow" (the product check) on 64-bit platforms.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 07:01:16 +01:00
8 changed files with 125 additions and 41 deletions

No files matched your search

+32 -12
View File
@@ -1024,7 +1024,7 @@ class binary_reader
{
std::uint64_t len{};
std::size_t size{};
return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_cbor_array(size);
return get_cbor_argument(len) && get_container_size(len, size, "array") && enter_cbor_array(size);
}
case 0x9F: // array (indefinite length)
@@ -1064,7 +1064,7 @@ class binary_reader
{
std::uint64_t len{};
std::size_t size{};
return get_cbor_argument(len) && get_cbor_container_size(len, size, "map") && enter_object(size);
return get_cbor_argument(len) && get_container_size(len, size, "map") && enter_object(size);
}
case 0xBF: // map (indefinite length)
@@ -1700,7 +1700,7 @@ class binary_reader
}
/*!
@brief narrow a definite CBOR array/map length to std::size_t
@brief narrow a definite CBOR or MessagePack array/map length to std::size_t
A definite length is rejected if it does not fit in std::size_t or if it
equals detail::unknown_size(), which is reserved to mark an indefinite-
@@ -1713,7 +1713,7 @@ class binary_reader
@param[in] context "array" or "map", for the error message
@return whether the length is usable
*/
bool get_cbor_container_size(const std::uint64_t len, std::size_t& result, const char* context)
bool get_container_size(const std::uint64_t len, std::size_t& result, const char* context)
{
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(len) || len == detail::unknown_size()))
{
@@ -2309,7 +2309,8 @@ class binary_reader
case 0xDD: // array 32
{
std::uint32_t len{};
return get_number(len) && enter_msgpack_array(conditional_static_cast<std::size_t>(len));
std::size_t size{};
return get_number(len) && get_container_size(len, size, "array") && enter_msgpack_array(size);
}
case 0xDE: // map 16
@@ -2321,7 +2322,8 @@ class binary_reader
case 0xDF: // map 32
{
std::uint32_t len{};
return get_number(len) && enter_object(conditional_static_cast<std::size_t>(len));
std::size_t size{};
return get_number(len) && get_container_size(len, size, "map") && enter_object(size);
}
// negative fixint
@@ -3131,6 +3133,27 @@ class binary_reader
return true;
}
/*!
@brief store the count of an optimized container (or the length of a string)
A count equal to npos is rejected: npos marks a container whose count is
not known, so a count of that value would be read as "no count" (compare
get_container_size()). No container can hold that many elements.
@param[in] count the count read from the input
@param[out] result set to @a count unless it is rejected
@return whether @a count is usable
*/
bool store_ubjson_count(const std::size_t count, std::size_t& result)
{
if (JSON_HEDLEY_UNLIKELY(count == npos))
{
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = count;
return true;
}
/*!
@brief read a UBJSON/BJData optimized-container count of a signed marker
type ('i', 'I', 'l', 'L') and narrow it to std::size_t
@@ -3161,8 +3184,7 @@ class binary_reader
{
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = static_cast<std::size_t>(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char
return true;
return store_ubjson_count(static_cast<std::size_t>(number), result); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char
}
/*!
@@ -3239,8 +3261,7 @@ class binary_reader
{
return false;
}
result = conditional_static_cast<std::size_t>(number);
return true;
return store_ubjson_count(conditional_static_cast<std::size_t>(number), result);
}
case 'M':
@@ -3258,8 +3279,7 @@ class binary_reader
{
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = detail::conditional_static_cast<std::size_t>(number);
return true;
return store_ubjson_count(detail::conditional_static_cast<std::size_t>(number), result);
}
case '[':
+2 -6
View File
@@ -251,12 +251,8 @@ std::string diagnostics(const error_site site)
std::string context;
#if JSON_DIAGNOSTICS
std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found)
{
// a value that is not found has no path, and with it no bytes
return context;
}
// a value that is not found leaves no tokens, like the root
static_cast<void>(edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens));
if (!tokens.empty())
{
// the root value has no path, but still has a byte range
+32 -12
View File
@@ -19039,7 +19039,7 @@ class binary_reader
{
std::uint64_t len{};
std::size_t size{};
return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_cbor_array(size);
return get_cbor_argument(len) && get_container_size(len, size, "array") && enter_cbor_array(size);
}
case 0x9F: // array (indefinite length)
@@ -19079,7 +19079,7 @@ class binary_reader
{
std::uint64_t len{};
std::size_t size{};
return get_cbor_argument(len) && get_cbor_container_size(len, size, "map") && enter_object(size);
return get_cbor_argument(len) && get_container_size(len, size, "map") && enter_object(size);
}
case 0xBF: // map (indefinite length)
@@ -19715,7 +19715,7 @@ class binary_reader
}
/*!
@brief narrow a definite CBOR array/map length to std::size_t
@brief narrow a definite CBOR or MessagePack array/map length to std::size_t
A definite length is rejected if it does not fit in std::size_t or if it
equals detail::unknown_size(), which is reserved to mark an indefinite-
@@ -19728,7 +19728,7 @@ class binary_reader
@param[in] context "array" or "map", for the error message
@return whether the length is usable
*/
bool get_cbor_container_size(const std::uint64_t len, std::size_t& result, const char* context)
bool get_container_size(const std::uint64_t len, std::size_t& result, const char* context)
{
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(len) || len == detail::unknown_size()))
{
@@ -20324,7 +20324,8 @@ class binary_reader
case 0xDD: // array 32
{
std::uint32_t len{};
return get_number(len) && enter_msgpack_array(conditional_static_cast<std::size_t>(len));
std::size_t size{};
return get_number(len) && get_container_size(len, size, "array") && enter_msgpack_array(size);
}
case 0xDE: // map 16
@@ -20336,7 +20337,8 @@ class binary_reader
case 0xDF: // map 32
{
std::uint32_t len{};
return get_number(len) && enter_object(conditional_static_cast<std::size_t>(len));
std::size_t size{};
return get_number(len) && get_container_size(len, size, "map") && enter_object(size);
}
// negative fixint
@@ -21146,6 +21148,27 @@ class binary_reader
return true;
}
/*!
@brief store the count of an optimized container (or the length of a string)
A count equal to npos is rejected: npos marks a container whose count is
not known, so a count of that value would be read as "no count" (compare
get_container_size()). No container can hold that many elements.
@param[in] count the count read from the input
@param[out] result set to @a count unless it is rejected
@return whether @a count is usable
*/
bool store_ubjson_count(const std::size_t count, std::size_t& result)
{
if (JSON_HEDLEY_UNLIKELY(count == npos))
{
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = count;
return true;
}
/*!
@brief read a UBJSON/BJData optimized-container count of a signed marker
type ('i', 'I', 'l', 'L') and narrow it to std::size_t
@@ -21176,8 +21199,7 @@ class binary_reader
{
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = static_cast<std::size_t>(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char
return true;
return store_ubjson_count(static_cast<std::size_t>(number), result); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char
}
/*!
@@ -21254,8 +21276,7 @@ class binary_reader
{
return false;
}
result = conditional_static_cast<std::size_t>(number);
return true;
return store_ubjson_count(conditional_static_cast<std::size_t>(number), result);
}
case 'M':
@@ -21273,8 +21294,7 @@ class binary_reader
{
return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
}
result = detail::conditional_static_cast<std::size_t>(number);
return true;
return store_ubjson_count(detail::conditional_static_cast<std::size_t>(number), result);
}
case '[':
+2 -6
View File
@@ -5080,12 +5080,8 @@ std::string diagnostics(const error_site site)
std::string context;
#if JSON_DIAGNOSTICS
std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found)
{
// a value that is not found has no path, and with it no bytes
return context;
}
// a value that is not found leaves no tokens, like the root
static_cast<void>(edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens));
if (!tokens.empty())
{
// the root value has no path, but still has a byte range
+20
View File
@@ -97,3 +97,23 @@ TEST_CASE("CBOR")
}
}
}
TEST_CASE("MessagePack")
{
SECTION("parse errors")
{
SECTION("array/map size equal to the unknown-size sentinel")
{
// on 32-bit platforms an array 32/map 32 length of 0xFFFFFFFF aliases unknown_size()
std::vector<uint8_t> const varr = {0xDD, 0xFF, 0xFF, 0xFF, 0xFF};
std::vector<uint8_t> const vmap = {0xDF, 0xFF, 0xFF, 0xFF, 0xFF};
json _;
CHECK_THROWS_WITH_AS(_ = json::from_msgpack(varr), "[json.exception.out_of_range.408] syntax error while parsing MessagePack size: excessive array size", json::out_of_range&);
CHECK(json::from_msgpack(varr, true, false).is_discarded());
CHECK_THROWS_WITH_AS(_ = json::from_msgpack(vmap), "[json.exception.out_of_range.408] syntax error while parsing MessagePack size: excessive map size", json::out_of_range&);
CHECK(json::from_msgpack(vmap, true, false).is_discarded());
}
}
}
+24 -4
View File
@@ -3237,14 +3237,34 @@ TEST_CASE("BJData")
#endif
CHECK(json::from_bjdata(vM, true, false).is_discarded());
#if SIZE_MAX != 0xffffffff
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vMX), "[json.exception.out_of_range.408] syntax error while parsing BJData size: excessive ndarray size caused overflow", json::out_of_range&);
#else
// the first dimension is 2^64-1, which is rejected as a count (see below)
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vMX), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
#endif
CHECK(json::from_bjdata(vMX, true, false).is_discarded());
}
SECTION("optimized array: count equal to SIZE_MAX")
{
// the reader marks a container without a count by SIZE_MAX, so
// a count of that value must not be read as "no count"
// (it used to parse as [1])
json _;
#if SIZE_MAX != 0xffffffff
std::vector<uint8_t> const vM = {'[', '#', 'M', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vM), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
CHECK(json::from_bjdata(vM, true, false).is_discarded());
// one less is an ordinary excessive count
std::vector<uint8_t> const vM1 = {'[', '#', 'M', 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vM1), "[json.exception.out_of_range.408] excessive array size: 18446744073709551614", json::out_of_range&);
#else
std::vector<uint8_t> const vm = {'[', '#', 'm', 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vm), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
CHECK(json::from_bjdata(vm, true, false).is_discarded());
std::vector<uint8_t> const vL = {'[', '#', 'L', 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 'i', 0x01, ']'};
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vL), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&);
CHECK(json::from_bjdata(vL, true, false).is_discarded());
#endif
}
SECTION("optimized array: integer value overflow")
{
#if SIZE_MAX == 0xffffffff
+1 -1
View File
@@ -1009,7 +1009,7 @@ TEST_CASE("regression test - excessive binary container size honors allow_except
// allow_exceptions=true (the default) must still throw exactly as before.
// The exact message text is not checked here: on platforms where
// std::size_t is 32-bit, the CBOR reader's own length-narrowing check
// (get_cbor_container_size(), unrelated to this fix) intercepts a
// (get_container_size(), unrelated to this fix) intercepts a
// declared length of 2^63 before it ever reaches the check this test
// targets, with different (but equally valid, and already correct)
// wording -- see unit-cbor.cpp for coverage of that message.
+12
View File
@@ -2385,6 +2385,18 @@ TEST_CASE("UBJSON optimized arrays of a valueless type are bounded")
}
}
SECTION("a count equal to SIZE_MAX is rejected")
{
// the reader marks a container without a count by SIZE_MAX, so a
// count of that value must not be read as "no count"; UBJSON's
// largest count type (int64) can only reach it where size_t is 32 bits
#if SIZE_MAX == 0xffffffff
const std::vector<uint8_t> input = {'[', '#', 'L', 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'};
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(input), "[json.exception.out_of_range.408] syntax error while parsing UBJSON size: integer value overflow", json::out_of_range&);
CHECK(json::from_ubjson(input, true, false).is_discarded());
#endif
}
SECTION("ordinary counts are unaffected")
{
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '$', 'Z', '#', 'i', 3})) == json({nullptr, nullptr, nullptr}));