Compare commits

..
Author SHA1 Message Date
Niels Lohmann f0a93bbf6f Throw std::length_error for operator[](SIZE_MAX) instead of corrupting the array
For idx == SIZE_MAX, the non-const array operator[] computed the new size as
idx + 1, which wraps to 0. resize(0) then emptied the array, and the
subsequent operator[](idx) on the now-empty vector wrote one element before
its buffer. Every other too-large index (e.g. SIZE_MAX - 1) already went
through resize(), which throws std::length_error and leaves the array
unchanged; SIZE_MAX was the one value for which the overflow bypassed that
safety net.

Add a guard that throws std::length_error before computing idx + 1 when idx
is the largest representable size_type value, so the array is left
unchanged, matching the exception vector::resize() already throws for
smaller (but still too large) indices.

Fixes #5647.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 23:21:37 +02:00
7 changed files with 44 additions and 70 deletions
@@ -70,6 +70,9 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. The function can throw the following exceptions:
- Throws [`type_error.305`](../../home/exceptions.md#jsonexceptiontype_error305) if the JSON value is not an array
or null; in that case, using the `[]` operator with an index makes no sense.
- Throws `#!cpp std::length_error` if `idx` equals the maximum value of `size_type`; the array is left unchanged.
(This is the one index for which growing the array to hold it cannot be expressed as a `size_type` size, the same
way an oversized [`resize`](https://en.cppreference.com/w/cpp/container/vector/resize) throws.)
2. The function can throw the following exceptions:
- Throws [`type_error.305`](../../home/exceptions.md#jsonexceptiontype_error305) if the JSON value is not an object
or null; in that case, using the `[]` operator with a key makes no sense.
@@ -257,7 +260,8 @@ Strong exception safety: if an exception occurs, the original value stays intact
## Version history
1. Added in version 1.0.0.
1. Added in version 1.0.0. Fixed in version 3.13.0 to throw `#!cpp std::length_error` instead of emptying the array and
accessing it out of bounds when `idx` equals the maximum value of `size_type`.
2. Added in version 1.0.0. Added overloads for `T* key` in version 1.1.0. Removed overloads for `T* key` (replaced by 3)
in version 3.11.0.
3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as
@@ -50,11 +50,9 @@ The default value is `0` (disabled — existing behavior is preserved).
```
Code that relies on these producing arrays must use `json::array()` instead (see below). Lists with more than one
element, and a single `[string, value]` pair *written as a braced list*, such as `{{"key", "value"}}`, which still
creates an object, are not affected. This exception is based on how the pair is written, not on the shape of its
value: an existing JSON value that happens to be a two-element array with a string as its first element, such as
`json arr = {"key", 42};`, is still copied by `json j{arr};` rather than turned into an object. The library's own
conversions are not affected either: for example, `std::tuple<int>{5}` still becomes `[5]`.
element, and a single `[string, value]` pair such as `{{"key", "value"}}`, which still creates an object, are not
affected. The library's own conversions are not affected either: for example, `std::tuple<int>{5}` still becomes
`[5]`.
!!! note "ABI compatibility"
-9
View File
@@ -34,7 +34,6 @@ class json_ref
json_ref(std::initializer_list<json_ref> init)
: owned_value(init)
, braced_list(true)
{}
template <
@@ -70,17 +69,9 @@ class json_ref
return &** this;
}
/// whether the value was written as a braced list, such as {"key", 1},
/// rather than given as a value
bool is_braced_list() const noexcept
{
return braced_list;
}
private:
mutable value_type owned_value = nullptr;
value_type const* value_ref = nullptr;
bool braced_list = false;
};
} // namespace detail
+9 -12
View File
@@ -36,7 +36,9 @@
#include <iosfwd> // istream, ostream
#endif // JSON_NO_IO
#include <iterator> // make_move_iterator, random_access_iterator_tag
#include <limits> // numeric_limits
#include <memory> // unique_ptr
#include <stdexcept> // length_error
#include <string> // string, stoi, to_string
#include <utility> // declval, forward, move, pair, swap
#include <vector> // vector
@@ -1672,18 +1674,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
bool type_deduction = true,
value_t manual_type = value_t::array)
{
#if JSON_BRACE_INIT_COPY_SEMANTICS
// a single element that is a value rather than a braced list is
// copied or moved as is, whatever its content looks like
if (type_deduction && init.size() == 1 && !init.begin()->is_braced_list())
{
*this = init.begin()->moved_or_copied();
set_parents();
assert_invariant();
return;
}
#endif
// check if each element is an array with two elements whose first
// element is a string
bool is_an_object = std::all_of(init.begin(), init.end(),
@@ -2842,6 +2832,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// fill up the array with null values if given idx is outside the range
if (idx >= m_data.m_value.array->size())
{
// idx + 1 would overflow size_type and wrap to 0, which would empty
// the array instead of growing it; reject such an idx the same way
// resize() rejects other indices that are too large to represent
if (JSON_HEDLEY_UNLIKELY(idx == (std::numeric_limits<size_type>::max)()))
{
JSON_THROW(std::length_error(detail::concat("array index ", std::to_string(idx), " exceeds size_type")));
}
#if JSON_DIAGNOSTICS
// remember array size & capacity before resizing
const auto old_size = m_data.m_value.array->size();
+9 -21
View File
@@ -36,7 +36,9 @@
#include <iosfwd> // istream, ostream
#endif // JSON_NO_IO
#include <iterator> // make_move_iterator, random_access_iterator_tag
#include <limits> // numeric_limits
#include <memory> // unique_ptr
#include <stdexcept> // length_error
#include <string> // string, stoi, to_string
#include <utility> // declval, forward, move, pair, swap
#include <vector> // vector
@@ -20047,7 +20049,6 @@ class json_ref
json_ref(std::initializer_list<json_ref> init)
: owned_value(init)
, braced_list(true)
{}
template <
@@ -20083,17 +20084,9 @@ class json_ref
return &** this;
}
/// whether the value was written as a braced list, such as {"key", 1},
/// rather than given as a value
bool is_braced_list() const noexcept
{
return braced_list;
}
private:
mutable value_type owned_value = nullptr;
value_type const* value_ref = nullptr;
bool braced_list = false;
};
} // namespace detail
@@ -27762,18 +27755,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
bool type_deduction = true,
value_t manual_type = value_t::array)
{
#if JSON_BRACE_INIT_COPY_SEMANTICS
// a single element that is a value rather than a braced list is
// copied or moved as is, whatever its content looks like
if (type_deduction && init.size() == 1 && !init.begin()->is_braced_list())
{
*this = init.begin()->moved_or_copied();
set_parents();
assert_invariant();
return;
}
#endif
// check if each element is an array with two elements whose first
// element is a string
bool is_an_object = std::all_of(init.begin(), init.end(),
@@ -28932,6 +28913,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// fill up the array with null values if given idx is outside the range
if (idx >= m_data.m_value.array->size())
{
// idx + 1 would overflow size_type and wrap to 0, which would empty
// the array instead of growing it; reject such an idx the same way
// resize() rejects other indices that are too large to represent
if (JSON_HEDLEY_UNLIKELY(idx == (std::numeric_limits<size_type>::max)()))
{
JSON_THROW(std::length_error(detail::concat("array index ", std::to_string(idx), " exceeds size_type")));
}
#if JSON_DIAGNOSTICS
// remember array size & capacity before resizing
const auto old_size = m_data.m_value.array->size();
@@ -72,28 +72,6 @@ TEST_CASE("JSON_BRACE_INIT_COPY_SEMANTICS")
CHECK(j7 == json::array({1, 2}));
}
SECTION("single-element brace initialization copies a pair-shaped array value (#5662)")
{
// a JSON value that happens to be a 2-element array whose first
// element is a string must still be copied, not turned into an
// object; only a braced list written in the source, such as the
// inner {"key", "value"} of {{"key", "value"}}, describes an object
json const pair_shaped = json::array({"key", 42});
json const j1{pair_shaped};
CHECK(j1.is_array());
CHECK(j1 == pair_shaped);
json const j2 = {pair_shaped};
CHECK(j2.is_array());
CHECK(j2 == pair_shaped);
// the same holds for an rvalue of the same shape
json const j3{json::array({"key", 42})};
CHECK(j3.is_array());
CHECK(j3 == pair_shaped);
}
SECTION("what the macro does not change")
{
// lists with more than one element are unaffected
+18
View File
@@ -147,6 +147,24 @@ TEST_CASE("element access 1")
CHECK(j_const[7] == json({1, 2, 3}));
}
SECTION("SIZE_MAX index (#5647)")
{
// idx + 1 must not be computed for idx == SIZE_MAX: it wraps to 0,
// which would empty the array and then write out of bounds instead
// of growing it; reject it like an oversized resize() would and
// leave the array unchanged
const auto max_idx = (std::numeric_limits<json::size_type>::max)();
const std::string expected = "array index " + std::to_string(max_idx) + " exceeds size_type";
const json j_before = j; // NOLINT(performance-unnecessary-copy-initialization)
CHECK_THROWS_WITH_AS(j[max_idx] = 1, expected.c_str(), std::length_error&);
CHECK(j == j_before);
json j_empty = json::array();
CHECK_THROWS_WITH_AS(j_empty[max_idx] = 1, expected.c_str(), std::length_error&);
CHECK(j_empty == json::array());
}
SECTION("access on non-array type")
{
SECTION("null")