Compare commits

...
Author SHA1 Message Date
Niels Lohmann 61a37849c9 json_view: keep tokens without an integer digit out of the float batch
dump() batches float tokens of 16 or 17 digits with at least 8 fraction
digits. A damaged image loaded with image_check::bounds can record a
token with no digit before the point and 17 after it, which no JSON
number has. Its q = -17 is outside the range of locate_token(), which
then read fives[17] past its 17 entries; on x86-64 the resulting digits
had 18 digits and failed the assertion of write_decimal() (OSS-Fuzz
issue 583898810, json_view_image_fuzzer).

The batch now takes 8 to 16 fraction digits, which every valid token of
16 or 17 digits has, so it batches the same tokens of valid documents.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-10 08:21:39 +01:00
3 changed files with 50 additions and 8 deletions

No files matched your search

+6 -4
View File
@@ -393,7 +393,7 @@ struct depth_counter<false>
/*!
@brief where the double x nearest to a token w * 10^q lies, for 10^15 <= w <
10^17 and -17 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
10^17 and -16 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
dump() writes the shortest decimal in the rounding interval of x (the closest
one if there are several, see zmij::to_shortest()). Its digits are those of a
@@ -1258,13 +1258,15 @@ class view_serializer
}
for (; p != stop && batch.count != float_batch::capacity(); ++p)
{
// a token of the source: 16 or 17 digits, at least 8 after the
// point, no exponent, 32 bytes to read
// a token of the source: 16 or 17 digits, 8 to 16 after the
// point, no exponent, 32 bytes to read (17 after the point, with
// no digit before it, only in a damaged image loaded with
// image_check::bounds: beyond the range of locate_token())
const unsigned frac_digits = p->extra >> 8u;
const unsigned int_digits = p->extra & 0xFFu;
const unsigned count = int_digits + frac_digits;
if (p->kind == node_kind::number_float && (p->flags & node_flags::storage) == 0
&& count - 16u <= 1u && frac_digits >= 8 && src_end - (src + p->off) >= 33)
&& count - 16u <= 1u && frac_digits - 8u <= 8u && src_end - (src + p->off) >= 33)
{
const auto* const token = reinterpret_cast<const unsigned char*>(src + p->off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const unsigned sign = token[0] == '-' ? 1 : 0;
+6 -4
View File
@@ -8841,7 +8841,7 @@ struct depth_counter<false>
/*!
@brief where the double x nearest to a token w * 10^q lies, for 10^15 <= w <
10^17 and -17 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
10^17 and -16 <= q <= -1 (Eisel-Lemire, then a fixed-point position)
dump() writes the shortest decimal in the rounding interval of x (the closest
one if there are several, see zmij::to_shortest()). Its digits are those of a
@@ -9706,13 +9706,15 @@ class view_serializer
}
for (; p != stop && batch.count != float_batch::capacity(); ++p)
{
// a token of the source: 16 or 17 digits, at least 8 after the
// point, no exponent, 32 bytes to read
// a token of the source: 16 or 17 digits, 8 to 16 after the
// point, no exponent, 32 bytes to read (17 after the point, with
// no digit before it, only in a damaged image loaded with
// image_check::bounds: beyond the range of locate_token())
const unsigned frac_digits = p->extra >> 8u;
const unsigned int_digits = p->extra & 0xFFu;
const unsigned count = int_digits + frac_digits;
if (p->kind == node_kind::number_float && (p->flags & node_flags::storage) == 0
&& count - 16u <= 1u && frac_digits >= 8 && src_end - (src + p->off) >= 33)
&& count - 16u <= 1u && frac_digits - 8u <= 8u && src_end - (src + p->off) >= 33)
{
const auto* const token = reinterpret_cast<const unsigned char*>(src + p->off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const unsigned sign = token[0] == '-' ? 1 : 0;
+38
View File
@@ -1165,6 +1165,44 @@ TEST_CASE("json_view images: check")
}
}
SECTION("float tokens with 17 digits after the point and none before it")
{
// No JSON number has this layout. dump() batches tokens of 16 or 17
// digits, but only those with 8 to 16 fraction digits are in its
// range: 17 read past the table of powers of five (caught by a
// bounds-checked std::array), and on x86-64 wrote 18 digits
// (the assertion of write_decimal()).
const std::vector<std::uint8_t> img = json_document::parse("[1.2345678901234567]" + std::string(40, ' ')).save();
node n = node_at(img, 1);
REQUIRE(n.len == 18);
n.extra = 17u << 8u;
std::vector<std::uint8_t> b = img;
set_node(b, 1, n);
std::memcpy(b.data() + text_at(img) + n.off, ".99999999999999999", 18);
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());
CHECK(dumped == d.root().materialize().dump());
// (the input of OSS-Fuzz issue 583898810: the token is " " and
// 15 nines, then "1{")
const std::vector<std::uint8_t> fuzzed =
{
0x4E, 0x4A, 0x56, 0x49, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0x10, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x20, 0x20, 0x20, 0x20,
0x20, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
0x31, 0x7B, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x00, 0x00
};
CHECK(load_result(fuzzed, image_check::full) == check_failed);
const json_document f = json_document::load(fuzzed, image_check::bounds);
CHECK_NOTHROW(f.root().dump());
}
SECTION("nodes that share a range")
{
// [big string, then n strings made to point to the big string]: every