Report an empty array for UBJSON [$N#<n>] instead of n elements (#6079)

* Report an empty array for UBJSON `[$N#<n>` instead of n elements

A no-op (N) is not a value, so an optimized array of type N holds no
element whatever count it declares. The reader skipped the elements but
still passed the declared count to the SAX parser: a handler was told
start_array(n) and no element followed, and the DOM parser reserved
min(n, 16384) values (256 KB) for an array that stays empty. Nine bytes
(`[$N#l 0x7FFFFFFF`) therefore committed 256 KB: 2000 of them in one
input, 18 KB, committed 512 MB of heap (capacity 163,840,000 values for
the 10000 arrays of the original probe: 2.6 GB for 70 KB of input).
Untouched pages keep the resident size small, but the commit charge and
a 32-bit address space are consumed.

The reader now announces 0 elements for an optimized array of type N.
The loop that skips the elements is unchanged (it never ran). A count
that does not fit into std::size_t is still rejected by the count
reader; a count that is too large for the array type of the DOM (before:
out_of_range.408 "excessive array size" from the DOM handler) is
accepted now, because the array is empty.

Behaviour change: sax_parse reports start_array(0) for `[$N#n` (before:
n); from_ubjson no longer throws out_of_range.408 for an N-typed array
whose count exceeds the maximum size of the array type.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Clarify the reservation note for announced array counts

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-11-11 22:37:23 +01:00
1 parent 71ec3b4306
commit 5187b6ecc7
6 files changed
+116 -26

No files matched your search

+84
View File
@@ -2691,6 +2691,90 @@ TEST_CASE("UBJSON the valueless elements of a whole input are bounded")
}
}
TEST_CASE("UBJSON an optimized array of no-ops is empty whatever its count")
{
// A no-op ('N') is not a value, so `[$N#<n>` is an array without elements
// for every n. The count used to be passed on to the SAX parser, which
// told it to expect n elements, and the DOM parser reserved storage for
// them: 16384 values (256 KB) for a count of 2^31 in 9 bytes of input.
SECTION("the SAX parser is told that the array is empty")
{
for (const std::vector<std::uint8_t>& input :
{
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'i', 2},
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'I', 0x7F, 0xFF},
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF},
std::vector<std::uint8_t> {'[', '$', 'N', '#', 'i', 0}
})
{
SaxEventLogger logger;
CHECK(json::sax_parse(input, &logger, json::input_format_t::ubjson));
CHECK(logger.events == std::vector<std::string>({"start_array(0)", "end_array()"}));
}
// between other values, the next value is read as usual
const std::vector<std::uint8_t> nested = {'[', '[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF, 'T', '[', '$', 'N', '#', 'i', 3, ']'};
SaxEventLogger logger;
CHECK(json::sax_parse(nested, &logger, json::input_format_t::ubjson));
CHECK(logger.events == std::vector<std::string>({"start_array()", "start_array(0)", "end_array()", "boolean(true)", "start_array(0)", "end_array()", "end_array()"}));
}
SECTION("the DOM parser reserves nothing")
{
// 2000 arrays of 9 bytes that announce 2^31 elements each: with a
// reservation of 16384 values per array this took about 500 MB
std::vector<std::uint8_t> input = {'['};
for (int i = 0; i < 2000; ++i)
{
input.insert(input.end(), {'[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF});
}
input.push_back(']');
const json j = json::from_ubjson(input);
REQUIRE(j.size() == 2000);
std::size_t capacity = 0;
for (const auto& element : j)
{
CHECK(element.is_array());
CHECK(element.empty());
capacity += element.get_ref<const json::array_t&>().capacity();
}
CHECK(capacity == 0);
// the 7-byte form inside an optimized array of arrays
std::vector<std::uint8_t> typed = {'[', '$', '[', '#', 'I', 0x07, 0xD0};
for (int i = 0; i < 2000; ++i)
{
typed.insert(typed.end(), {'$', 'N', '#', 'I', 0x7F, 0xFF});
}
const json k = json::from_ubjson(typed);
REQUIRE(k.size() == 2000);
capacity = 0;
for (const auto& element : k)
{
CHECK(element.empty());
capacity += element.get_ref<const json::array_t&>().capacity();
}
CHECK(capacity == 0);
}
SECTION("a count that no container could hold is no longer an error")
{
// the array is empty, so nothing has to fit: 0x7FFFFFFF used to be
// reported as out_of_range.408 where it exceeds the maximum size of
// an array, and is accepted now
const std::vector<std::uint8_t> input = {'[', '$', 'N', '#', 'l', 0x7F, 0xFF, 0xFF, 0xFF};
CHECK(json::from_ubjson(input) == json::array());
// a count that does not fit into std::size_t still is
#if SIZE_MAX == 0xffffffff
const std::vector<std::uint8_t> too_large = {'[', '$', 'N', '#', 'L', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
json _;
CHECK_THROWS_AS(_ = json::from_ubjson(too_large), json::out_of_range&);
#endif
}
}
TEST_CASE("issue #5405 - array reserve for definite-length UBJSON arrays")
{
#if !defined(JSON_NOEXCEPTION)