diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index 5ce68220d..ccb54c3d7 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -9,7 +9,7 @@ #pragma once -#include // find, find_if, max +#include // find, find_if, max, min #include // array #include // size_t, ptrdiff_t #include // int64_t, uint8_t, uint16_t, uint32_t, uint64_t @@ -195,8 +195,18 @@ class builder const std::uint64_t done = static_cast(at - b) + 1; const std::uint64_t guess = static_cast(n) * static_cast(e - b + 1) / done; const std::uint64_t grown = guess + (guess / 4) + 64; // a variable: GCC calls a cast of the sum useless where std::uint64_t is std::size_t + // (n is below 2^32: the input is smaller than 4 GiB; the sum cannot wrap) + const std::uint64_t wanted = (std::max)(grown, static_cast(n) + (n / 2) + 64); + const std::uint64_t limit = document_data::max_nodes(); doc.tape_size = n; - doc.reserve((std::max)(static_cast(grown), n + (n / 2) + 64)); + // LCOV_EXCL_START (a node array that fills the address space) + if (NLOHMANN_VIEW_UNLIKELY(n >= limit)) + { + document_data::throw_bad_alloc(); // no room for another node + } + // LCOV_EXCL_STOP + // (a count beyond the limit is cut: the index does not grow beyond what can be addressed) + doc.reserve(static_cast((std::min)(wanted, limit))); return doc.tape; } diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index 66275702c..aa3958e41 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -11,7 +11,8 @@ #include // array #include // size_t #include // memcpy -#include // operator new, placement new +#include // numeric_limits +#include // bad_alloc, operator new, placement new #include // string #include @@ -84,13 +85,30 @@ struct document_data tape_cap = inline_cap; } - /// make room for n nodes; keeps the first tape_size nodes + /// the largest node count whose size in bytes fits a std::size_t + static constexpr std::size_t max_nodes() noexcept + { + return (std::numeric_limits::max)() / sizeof(node); + } + + [[noreturn]] NLOHMANN_VIEW_NOINLINE static void throw_bad_alloc() + { + NLOHMANN_VIEW_THROW(std::bad_alloc()); + } + + /// make room for n nodes; keeps the first tape_size nodes (throws + /// std::bad_alloc for a count that does not fit the address space, + /// instead of wrapping around in n * sizeof(node)) void reserve(std::size_t n) { if (n <= tape_cap) { return; } + if (NLOHMANN_VIEW_UNLIKELY(n > max_nodes())) + { + throw_bad_alloc(); + } node* fresh = static_cast(::operator new (n * sizeof(node))); if (tape_size != 0) { diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index c585eb919..8b8518c38 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -19,8 +19,10 @@ using nlohmann::json; #include #include +#include #include #include +#include #include #include #include @@ -459,3 +461,31 @@ TEST_CASE("json_view node integer bits") } } } + +TEST_CASE("json_view node array size limit") +{ + // a node array larger than the address space is refused, not wrapped to a + // small allocation (the size computation overflows on 32-bit targets, and + // for absurd counts everywhere) + std::unique_ptr d(document_data::create(0)); + d->reserve(8); + REQUIRE(d->tape_cap >= 8); + d->tape_size = 2; + const std::size_t cap = d->tape_cap; + node* const tape = d->tape; + +#if !defined(JSON_NOEXCEPTION) + const std::size_t too_many = document_data::max_nodes() + 1; + CHECK_THROWS_AS(d->reserve(too_many), std::bad_alloc&); + CHECK_THROWS_AS(d->reserve((std::numeric_limits::max)()), std::bad_alloc&); + // the array is unchanged + CHECK(d->tape == tape); + CHECK(d->tape_cap == cap); + CHECK(d->tape_size == 2); +#endif + + // the largest count that fits is not refused by the check (nothing is + // allocated for a count that is already there) + d->reserve(cap); + CHECK(d->tape == tape); +}