From 5b0171a70b51b70125d1c707406d981e32baca69 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 16:11:39 +0200 Subject: [PATCH] Fix out-of-bounds read when dumping a float token of an unchecked image A float node of an image loaded with image_check::bounds can hold a token whose bytes are not digits, so its value need not have the digit count recorded in the node. write_double_at() passed that count to write_short_decimal(), which indexes its table of powers of ten by it: an assertion failure in debug builds, an out-of-bounds read in release builds. Use the counted overload only if the value has exactly that many digits, otherwise count them. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/serializer.hpp | 11 ++++++--- tests/src/unit-json_view_image.cpp | 25 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp index 8ed9fc9ac..9fc654544 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -752,9 +752,14 @@ class view_serializer { *w = '-'; w += d.negative ? 1 : 0; - // (without leading zeros, all digits of the token count) - const unsigned char lead = first[d.negative ? 1 : 0]; - return lead != '0' ? ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast(int_digits + frac_digits), static_cast(d.exponent)) + // (without leading zeros, all digits of the token count; the + // check also keeps an image that was only checked for bounds, + // whose token may not be made of digits, from the counted + // overload) + const auto& powers = ::nlohmann::detail::dtoa_impl::powers_of_ten_16(); + const unsigned count = int_digits + frac_digits; + return count - 1u < 15u && d.w >= powers[count - 1u] && d.w < powers[count] + ? ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast(count), static_cast(d.exponent)) : ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast(d.exponent)); } return write_double_value_at(w, decimal_to_float(d)); // (without reading the token again) diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 6a615f7d4..eb511f52b 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -712,6 +712,31 @@ TEST_CASE("json_view images: check") } } + SECTION("float tokens with fewer digits than the layout records") + { + // The bytes of the token are not digits (they read as zeros or as + // other values), so the value has fewer (or more) digits than the + // layout says: dump() must still write a number. + for (const auto& source : std::vector> + { + {"123456789012345678.5", std::string("@") + std::string(16, '0') + "1.1"}, // 19 digits + {"1234.5", "@001.1"}, // 5 digits + {"1234.5", "9??.??"} // more than 5 digits + }) + { + CAPTURE(source.second) + const std::vector img = json_document::parse("[" + source.first + "]").save(); + const node n = node_at(img, 1); + REQUIRE(source.second.size() == n.len); + std::vector b = img; + std::memcpy(b.data() + text_at(img) + n.off, source.second.data(), n.len); + const json_document d = json_document::load(b, image_check::bounds); + const std::string dumped = d.root().dump(); + CAPTURE(dumped) + CHECK(json::parse(dumped)[0].is_number()); + } + } + SECTION("integer ranges") { // tokens of many digits, which the parser stores as floats