From bcd5af62c754a8c6274259a4c7f742945d7dd21c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 6 Sep 2026 17:05:39 +0200 Subject: [PATCH] Reject a nested BJData ndarray dimension vector where it is read get_ubjson_size_type() takes an inside_ndarray parameter saying whether it is being called for an ndarray's dimension vector, where another ndarray is not allowed. It then seeded the flag it passes down to get_ubjson_size_value() with `false` rather than with that parameter, and only consulted inside_ndarray afterwards, on the '$' branch. So on the '#' branch nothing stopped the descent: every "#[" pair of an input like "[" followed by "#[#[#[..." opened another dimension vector, several native stack frames deeper each time, and the recursion was only reported on the way back out. 100,000 pairs crash the process. This is #5104 again, in a path that has nothing to do with containers. Seed the flag with inside_ndarray, which is what get_ubjson_size_value() documents it wants: "for input, `true` means already inside an ndarray vector or ndarray dimension is not allowed". The nested '[' is then refused where it is read, so the length of the chain no longer matters. Both post-checks gain `&& !inside_ndarray`, because an ndarray was found *here* only if the flag flipped -- get_ubjson_size_value() only ever returns `true` when its initial value was `false`, as its documentation says. With that, the "ndarray can not be recursive" branch is unreachable: a recursive ndarray is now caught one level earlier, and reported as "ndarray dimensional vector is not allowed" like every other nested dimension vector. Three existing expectations move accordingly (vR2, vR4, vR6). All three now fail earlier, and all three now report the same error that vR1, vR5 and vH already reported for the same shape, which is the more consistent outcome. Everything else is unchanged: valid 1D and 2D ndarrays, optimized containers and plain arrays produce identical results, and unit-ubjson is untouched. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 19 ++++++++++------- single_include/nlohmann/json.hpp | 19 ++++++++++------- tests/src/unit-bjdata.cpp | 21 ++++++++++++++++--- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 1e05c08f3..f61aaf32f 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -2477,7 +2477,12 @@ class binary_reader { result.first = npos; // size result.second = 0; // type - bool is_ndarray = false; + // seed the flag with the caller's context: inside an ndarray dimension + // vector another ndarray is not allowed, and get_ubjson_size_value() + // rejects it up front instead of reading it and reporting afterwards. + // Seeding it with `false` made every '#' of a "[#[#[..." chain descend + // another level, which overflowed the stack (see #5104). + bool is_ndarray = inside_ndarray; get_ignore_noop(); @@ -2510,13 +2515,11 @@ class binary_reader } const bool is_error = get_ubjson_size_value(result.first, is_ndarray); - if (input_format == input_format_t::bjdata && is_ndarray) + // an ndarray was read here only if the flag flipped; when it was + // seeded true, get_ubjson_size_value() already rejected the nested + // dimension vector + if (input_format == input_format_t::bjdata && is_ndarray && !inside_ndarray) { - if (inside_ndarray) - { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, - exception_message(input_format, "ndarray can not be recursive", "size"), nullptr)); - } result.second |= (1 << 8); // use bit 8 to indicate ndarray, all UBJSON and BJData markers should be ASCII letters } return is_error; @@ -2525,7 +2528,7 @@ class binary_reader if (current == '#') { const bool is_error = get_ubjson_size_value(result.first, is_ndarray); - if (input_format == input_format_t::bjdata && is_ndarray) + if (input_format == input_format_t::bjdata && is_ndarray && !inside_ndarray) { return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, exception_message(input_format, "ndarray requires both type and size", "size"), nullptr)); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f2122e3ee..1720573c4 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -13164,7 +13164,12 @@ class binary_reader { result.first = npos; // size result.second = 0; // type - bool is_ndarray = false; + // seed the flag with the caller's context: inside an ndarray dimension + // vector another ndarray is not allowed, and get_ubjson_size_value() + // rejects it up front instead of reading it and reporting afterwards. + // Seeding it with `false` made every '#' of a "[#[#[..." chain descend + // another level, which overflowed the stack (see #5104). + bool is_ndarray = inside_ndarray; get_ignore_noop(); @@ -13197,13 +13202,11 @@ class binary_reader } const bool is_error = get_ubjson_size_value(result.first, is_ndarray); - if (input_format == input_format_t::bjdata && is_ndarray) + // an ndarray was read here only if the flag flipped; when it was + // seeded true, get_ubjson_size_value() already rejected the nested + // dimension vector + if (input_format == input_format_t::bjdata && is_ndarray && !inside_ndarray) { - if (inside_ndarray) - { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, - exception_message(input_format, "ndarray can not be recursive", "size"), nullptr)); - } result.second |= (1 << 8); // use bit 8 to indicate ndarray, all UBJSON and BJData markers should be ASCII letters } return is_error; @@ -13212,7 +13215,7 @@ class binary_reader if (current == '#') { const bool is_error = get_ubjson_size_value(result.first, is_ndarray); - if (input_format == input_format_t::bjdata && is_ndarray) + if (input_format == input_format_t::bjdata && is_ndarray && !inside_ndarray) { return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, exception_message(input_format, "ndarray requires both type and size", "size"), nullptr)); diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 7d0dd5ff2..6ff31e79e 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -3288,8 +3288,10 @@ TEST_CASE("BJData") CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vR1), "[json.exception.parse_error.113] parse error at byte 6: syntax error while parsing BJData size: ndarray dimensional vector is not allowed", json::parse_error&); CHECK(json::from_bjdata(vR1, true, false).is_discarded()); + // a dimension vector that opens another one is rejected where the + // nested '[' is read, rather than after it has been descended into std::vector const vR2 = {'[', '$', 'i', '#', '[', '#', '[', 'i', 1, ']', ']', 1}; - CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vR2), "[json.exception.parse_error.113] parse error at byte 11: syntax error while parsing BJData size: expected length type specification (U, i, u, I, m, l, M, L) after '#'; last byte: 0x5D", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vR2), "[json.exception.parse_error.113] parse error at byte 7: syntax error while parsing BJData size: ndarray dimensional vector is not allowed", json::parse_error&); CHECK(json::from_bjdata(vR2, true, false).is_discarded()); std::vector const vR3 = {'[', '#', '[', 'i', '2', 'i', 2, ']'}; @@ -3297,7 +3299,7 @@ TEST_CASE("BJData") CHECK(json::from_bjdata(vR3, true, false).is_discarded()); std::vector const vR4 = {'[', '$', 'i', '#', '[', '$', 'i', '#', '[', 'i', 1, ']', 1}; - CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vR4), "[json.exception.parse_error.110] parse error at byte 14: syntax error while parsing BJData number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vR4), "[json.exception.parse_error.113] parse error at byte 9: syntax error while parsing BJData size: ndarray dimensional vector is not allowed", json::parse_error&); CHECK(json::from_bjdata(vR4, true, false).is_discarded()); std::vector const vR5 = {'[', '$', 'i', '#', '[', '[', '[', ']', ']', ']'}; @@ -3305,12 +3307,25 @@ TEST_CASE("BJData") CHECK(json::from_bjdata(vR5, true, false).is_discarded()); std::vector const vR6 = {'[', '$', 'i', '#', '[', '$', 'i', '#', '[', 'i', '2', 'i', 2, ']'}; - CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vR6), "[json.exception.parse_error.112] parse error at byte 14: syntax error while parsing BJData size: ndarray can not be recursive", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vR6), "[json.exception.parse_error.113] parse error at byte 9: syntax error while parsing BJData size: ndarray dimensional vector is not allowed", json::parse_error&); CHECK(json::from_bjdata(vR6, true, false).is_discarded()); std::vector const vH = {'[', 'H', '[', '#', '[', '$', 'i', '#', '[', 'i', '2', 'i', 2, ']'}; CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vH), "[json.exception.parse_error.113] parse error at byte 3: syntax error while parsing BJData size: ndarray dimensional vector is not allowed", json::parse_error&); CHECK(json::from_bjdata(vH, true, false).is_discarded()); + + // Every "#[" of this chain used to open another dimension vector + // and cost several stack frames before anything was rejected, so a + // long enough chain crashed the process (see #5104). The nested + // vector is refused where it is read, so the length is irrelevant. + std::vector vRdeep = {'['}; + for (std::size_t i = 0; i < 100000; ++i) + { + vRdeep.push_back('#'); + vRdeep.push_back('['); + } + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vRdeep), "[json.exception.parse_error.113] parse error at byte 5: syntax error while parsing BJData size: ndarray dimensional vector is not allowed", json::parse_error&); + CHECK(json::from_bjdata(vRdeep, true, false).is_discarded()); } SECTION("objects")