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")