Compare commits

..
Author SHA1 Message Date
Niels Lohmann 71f3554243 Document that update() and merge_patch() must not alias *this
update() and merge_patch() read their argument while they modify
*this. When the argument is *this or a value nested inside *this
(for example a subobject returned by operator[]), the modification
destroys or relocates the value while it is still being iterated,
so the functions read freed memory or dereference invalidated
iterators (heap-use-after-free, or an uncaught invalid_iterator.214
for update()). This reproduces with plain std::map-backed json and,
for update() on ordered_json, also via reallocation of the
underlying vector.

A fix would require copying the argument whenever it may alias
*this, which cannot be checked in constant time without parent
pointers (only available under JSON_DIAGNOSTICS), and would cost an
unconditional deep copy per call otherwise. The maintainer decided
to document the restriction instead of changing the library.

Add a "Notes" section with a "!!! danger" admonition to
update.md and merge_patch.md explaining that the argument must not
be *this or refer into *this, and showing the workaround of passing
a copy, e.g. j.update(json(j["a"])) and j.merge_patch(json(j)).

Fixes #5641.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 23:14:33 +02:00
7 changed files with 53 additions and 66 deletions
@@ -37,6 +37,22 @@ Thereby, `Target` is the current object; that is, the patch is applied to the cu
Linear in the lengths of `apply_patch`.
## Notes
!!! danger "Undefined behavior"
`merge_patch()` reads `apply_patch` while it modifies `#!cpp *this`. `apply_patch` must not be `#!cpp *this`
itself and must not refer to a value contained in `#!cpp *this` (for example, a subobject returned by
`#!cpp (*this)[key]`). Calling `merge_patch()` with such an argument reads the argument after it has been
invalidated by the modification, which is undefined behavior. If the patch may alias `#!cpp *this`, pass a copy
instead:
```cpp
j.merge_patch(json(j)); // instead of j.merge_patch(j)
```
See [GitHub issue #5641](https://github.com/nlohmann/json/issues/5641) for more information.
## Examples
??? example
@@ -61,3 +77,5 @@ Linear in the lengths of `apply_patch`.
## Version history
- Added in version 3.0.0.
- Documented that `apply_patch` must not be `#!cpp *this` or refer to a value contained in `#!cpp *this`, in version
3.13.0.
@@ -43,9 +43,6 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
- Throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412) if the length of a document, array,
string, or binary value exceeds the range of the 32-bit BSON length field; example:
`"BSON length 2147483661 exceeds maximum of 2147483647"`
- Throws [`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415) if the subtype of a binary value
exceeds 255, the maximum of the BSON binary subtype; example:
`"subtype 70000 is too large for the BSON binary subtype (max 255)"`
## Complexity
@@ -81,4 +78,3 @@ pass before anything is written.
- Added in version 3.4.0.
- Linear in the size of `j`, and no longer limited by the call stack for deeply nested values, since version 3.13.0.
- `out_of_range.415` is now detected before anything is written, like the other exceptions above, since version 3.13.0.
+18
View File
@@ -59,6 +59,22 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value
1. O(N*log(size() + N)), where N is the number of elements to insert.
2. O(N*log(size() + N)), where N is the number of elements to insert.
## Notes
!!! danger "Undefined behavior"
Both overloads read the argument while they modify `#!cpp *this`. The argument `j` (or, for overload (2), the
range `[first, last)`) must not be `#!cpp *this` itself and must not refer to a value contained in
`#!cpp *this` (for example, a subobject returned by `#!cpp (*this)[key]`). Calling `update()` with such an
argument reads the argument after it has been invalidated by the modification, which is undefined behavior. If
the argument may alias `#!cpp *this`, pass a copy instead:
```cpp
j.update(json(j["defaults"])); // instead of j.update(j["defaults"])
```
See [GitHub issue #5641](https://github.com/nlohmann/json/issues/5641) for more information.
## Examples
??? example
@@ -155,3 +171,5 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value
- Added in version 3.0.0.
- Added `merge_objects` parameter in 3.10.5.
- Documented that the argument must not be `#!cpp *this` or refer to a value contained in `#!cpp *this`, in version
3.13.0.
@@ -1055,26 +1055,15 @@ class binary_writer
}
/*!
@return The size of the BSON-encoded binary array in @a j
@throw out_of_range.415 if the subtype of @a j does not fit into a byte,
before anything is written
@return The size of the BSON-encoded binary array @a value
*/
static std::size_t calc_bson_binary_size(const BasicJsonType& j)
static std::size_t calc_bson_binary_size(const typename BasicJsonType::binary_t& value)
{
const auto& value = *j.m_data.m_value.binary;
if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits<std::uint8_t>::max)()))
{
JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), &j));
}
return sizeof(std::int32_t) + value.size() + 1ul;
}
/*!
@brief Writes a BSON element with key @a name and binary value @a value
@pre @a value's subtype, if any, fits into a byte; @ref calc_bson_sizes
checks this for every binary value in the document beforehand.
*/
void write_bson_binary(const string_t& name,
const binary_t& value)
@@ -1083,6 +1072,11 @@ class binary_writer
write_number<std::int32_t>(to_bson_length(value.size()), true);
if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits<std::uint8_t>::max)()))
{
JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), nullptr));
}
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
oa.write_characters(reinterpret_cast<const CharType*>(value.data()), value.size());
@@ -1091,15 +1085,13 @@ class binary_writer
/*!
@return The size of the value of the BSON document entry for @a j, which
is neither an object nor an array
@throw out_of_range.415 if @a j is binary with a subtype that does not fit
into a byte, before anything is written
*/
static std::size_t calc_bson_value_size(const BasicJsonType& j)
{
switch (j.type())
{
case value_t::binary:
return calc_bson_binary_size(j);
return calc_bson_binary_size(*j.m_data.m_value.binary);
case value_t::boolean:
return 1ul;
@@ -1225,8 +1217,6 @@ class binary_writer
@return the size of @a document
@throw out_of_range.409 if a key contains U+0000, before anything is
written
@throw out_of_range.415 if a binary value's subtype does not fit into a
byte, before anything is written
*/
static std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector<std::size_t>& nested_sizes)
{
+8 -18
View File
@@ -21385,26 +21385,15 @@ class binary_writer
}
/*!
@return The size of the BSON-encoded binary array in @a j
@throw out_of_range.415 if the subtype of @a j does not fit into a byte,
before anything is written
@return The size of the BSON-encoded binary array @a value
*/
static std::size_t calc_bson_binary_size(const BasicJsonType& j)
static std::size_t calc_bson_binary_size(const typename BasicJsonType::binary_t& value)
{
const auto& value = *j.m_data.m_value.binary;
if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits<std::uint8_t>::max)()))
{
JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), &j));
}
return sizeof(std::int32_t) + value.size() + 1ul;
}
/*!
@brief Writes a BSON element with key @a name and binary value @a value
@pre @a value's subtype, if any, fits into a byte; @ref calc_bson_sizes
checks this for every binary value in the document beforehand.
*/
void write_bson_binary(const string_t& name,
const binary_t& value)
@@ -21413,6 +21402,11 @@ class binary_writer
write_number<std::int32_t>(to_bson_length(value.size()), true);
if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits<std::uint8_t>::max)()))
{
JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), nullptr));
}
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
oa.write_characters(reinterpret_cast<const CharType*>(value.data()), value.size());
@@ -21421,15 +21415,13 @@ class binary_writer
/*!
@return The size of the value of the BSON document entry for @a j, which
is neither an object nor an array
@throw out_of_range.415 if @a j is binary with a subtype that does not fit
into a byte, before anything is written
*/
static std::size_t calc_bson_value_size(const BasicJsonType& j)
{
switch (j.type())
{
case value_t::binary:
return calc_bson_binary_size(j);
return calc_bson_binary_size(*j.m_data.m_value.binary);
case value_t::boolean:
return 1ul;
@@ -21555,8 +21547,6 @@ class binary_writer
@return the size of @a document
@throw out_of_range.409 if a key contains U+0000, before anything is
written
@throw out_of_range.415 if a binary value's subtype does not fit into a
byte, before anything is written
*/
static std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector<std::size_t>& nested_sizes)
{
+1 -20
View File
@@ -797,11 +797,7 @@ TEST_CASE("regression test - BSON binary subtype rejects a value that doesn't fi
CHECK(json::from_bson(json::to_bson(doc255))["b"].get_binary().subtype() == 255);
CHECK_THROWS_AS(json::to_bson(json{{"b", json::binary({1, 2}, 256)}}), json::out_of_range);
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(json::to_bson(json {{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] (/b) subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range);
#else
CHECK_THROWS_WITH_AS(json::to_bson(json {{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range);
#endif
CHECK_THROWS_WITH_AS(json::to_bson(json{{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range);
}
TEST_CASE("BSON input/output_adapters")
@@ -1809,21 +1805,6 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} :
CHECK(output.empty());
}
SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)")
{
// the offending value is nested, so this also covers that the check
// is not limited to a directly written value's own document
json const j = {{"a", {{"b", json::binary({1, 2}, 300)}}}};
std::vector<std::uint8_t> vector_output;
CHECK_THROWS_AS(json::to_bson(j, vector_output), json::out_of_range&);
CHECK(vector_output.empty());
std::string string_output;
CHECK_THROWS_AS(json::to_bson(j, string_output), json::out_of_range&);
CHECK(string_output.empty());
}
SECTION("values nested too deeply for the call stack (#5392)")
{
// serializing recursed once per nesting level, and computed every
-6
View File
@@ -101,12 +101,6 @@ TEST_CASE("Regression tests for extended diagnostics")
CHECK_THROWS_WITH_AS(j.unflatten(), "[json.exception.type_error.315] (/~1foo) values in object must be primitive", json::type_error);
}
SECTION("Regression test for issue #5675 - to_bson: out_of_range.415 has no diagnostics context")
{
json const j = {{"a", {{"b", json::binary({1, 2}, 300)}}}};
CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.out_of_range.415] (/a/b) subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range);
}
SECTION("Regression test for issue #2838 - Assertion failure when inserting into arrays with JSON_DIAGNOSTICS set")
{
// void push_back(basic_json&& val)