Fix update() and merge_patch() when the argument is *this or one of its members (#5678)

This commit is contained in:
Niels Lohmann
2026-09-30 23:04:57 +02:00
committed by GitHub
parent 1d675cdb46
commit a32f61eb98
7 changed files with 295 additions and 66 deletions

View File

@@ -37,6 +37,12 @@ Thereby, `Target` is the current object; that is, the patch is applied to the cu
Linear in the lengths of `apply_patch`.
## Notes
`apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject
returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of
`#!cpp *this`.
## Examples
??? example
@@ -61,3 +67,5 @@ Linear in the lengths of `apply_patch`.
## Version history
- Added in version 3.0.0.
- Fixed use of freed or relocated memory when `apply_patch` is `#!cpp *this` or refers to a value contained in
`#!cpp *this`, in version 3.13.0.

View File

@@ -59,6 +59,12 @@ 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
The argument `j` (or, for overload (2), the range `[first, last)`) may be `#!cpp *this` itself or refer to a value
contained in `#!cpp *this` (for example, a subobject returned by `#!cpp (*this)[key]`); it is read as it was when
`update()` was called, before any modification of `#!cpp *this`.
## Examples
??? example
@@ -155,3 +161,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.
- Fixed use of freed or relocated memory when the argument is `#!cpp *this` or refers to a value contained in
`#!cpp *this`, in version 3.13.0.

View File

@@ -4415,25 +4415,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/update/
void update(const_reference j, bool merge_objects = false)
{
update(j.begin(), j.end(), merge_objects);
prepare_update();
// passed value must be an object (checked here so a type_error names
// j, not the copy made below)
if (JSON_HEDLEY_UNLIKELY(!j.is_object()))
{
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", j.type_name()), &j));
}
// copy first: j may be *this or one of its descendants, and is
// iterated (and moved from) below to update *this
basic_json source = j;
update_from(source, merge_objects);
}
/// @brief updates a JSON object from another object, overwriting existing keys
/// @sa https://json.nlohmann.me/api/basic_json/update/
void update(const_iterator first, const_iterator last, bool merge_objects = false) // NOLINT(performance-unnecessary-value-param)
{
// implicitly convert a null value to an empty object
if (is_null())
{
m_data.m_type = value_t::object;
m_data.m_value.object = create<object_t>();
assert_invariant();
}
if (JSON_HEDLEY_UNLIKELY(!is_object()))
{
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this));
}
prepare_update();
// check if range iterators belong to the same JSON object
if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object))
@@ -4447,7 +4448,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", first.m_object->type_name()), first.m_object));
}
update_members(first, last, merge_objects, 0);
// copy first: the range may belong to *this or one of its
// descendants, and is iterated (and moved from) below to update
// *this
basic_json source(first, last);
update_from(source, merge_objects);
}
private:
@@ -4455,15 +4460,42 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// merge_patch_iteratively is merging into, and the members still to merge
struct merge_frame
{
merge_frame(basic_json* target_, const_iterator position_, const_iterator last_) noexcept
merge_frame(basic_json* target_, iterator position_, iterator last_) noexcept
: target(target_), position(std::move(position_)), last(std::move(last_))
{}
basic_json* target;
const_iterator position;
const_iterator last;
iterator position;
iterator last;
};
/// @brief converts a null value to an empty object and checks that this
/// value is an object; called first by both @ref update overloads
void prepare_update()
{
// implicitly convert a null value to an empty object; create the
// object before setting the type, so a throwing allocation leaves
// this value null
if (is_null())
{
m_data.m_value.object = create<object_t>();
m_data.m_type = value_t::object;
assert_invariant();
}
if (JSON_HEDLEY_UNLIKELY(!is_object()))
{
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this));
}
}
/// @brief starts the @ref update_members loop over an already-copied @a
/// source; called by both @ref update overloads
void update_from(basic_json& source, const bool merge_objects)
{
update_members(source.begin(), source.end(), merge_objects, 0);
}
/*!
@brief the members loop of @ref update, for this object and range
@@ -4476,7 +4508,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
@param[in] depth nesting level of this object, counted from the object
@ref update was called on
*/
void update_members(const const_iterator& first, const const_iterator& last, const bool merge_objects, const std::size_t depth)
void update_members(const iterator& first, const iterator& last, const bool merge_objects, const std::size_t depth)
{
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
@@ -4494,13 +4526,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// are overwritten as usual" behavior (see #5402).
if (it2 != m_data.m_value.object->end() && it2->second.is_object())
{
it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1);
it2->second.update_members(it.value().begin(), it.value().end(), true, depth + 1);
continue;
}
}
// set_parent() also repairs the other members, which ordered_json
// relocates when adding a key makes its vector grow
set_parent(m_data.m_value.object->operator[](it.key()) = it.value());
set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value()));
}
}
@@ -4514,7 +4546,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
version. Only reached for values nested deeper than @ref
detail::recursion_depth_limit.
*/
void update_members_iteratively(const_iterator first, const_iterator last)
void update_members_iteratively(iterator first, iterator last)
{
std::vector<merge_frame> stack;
@@ -4541,18 +4573,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const auto it2 = target->m_data.m_value.object->find(first.key());
if (it2 != target->m_data.m_value.object->end() && it2->second.is_object())
{
const basic_json& source = first.value();
basic_json& source = first.value();
++first;
stack.emplace_back(target, first, last);
target = &it2->second;
first = source.cbegin();
last = source.cend();
first = source.begin();
last = source.end();
continue;
}
}
// set_parent() also repairs the other members, which ordered_json
// relocates when adding a key makes its vector grow
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = first.value());
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value()));
++first;
}
}
@@ -6561,7 +6593,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/merge_patch/
void merge_patch(const basic_json& apply_patch)
{
apply_merge_patch(apply_patch, 0);
// copy first: apply_patch may be *this or one of its descendants,
// and is iterated (and moved from) below to patch *this
basic_json patch = apply_patch;
apply_merge_patch(patch, 0);
}
private:
@@ -6574,7 +6609,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::recursion_depth_limit levels have been entered, @ref
merge_patch_iteratively applies what is left without the call stack.
*/
void apply_merge_patch(const basic_json& apply_patch, const std::size_t depth)
void apply_merge_patch(basic_json& apply_patch, const std::size_t depth)
{
if (apply_patch.is_object())
{
@@ -6602,7 +6637,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
else
{
*this = apply_patch;
*this = std::move(apply_patch);
}
}
@@ -6615,12 +6650,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
recursive version. Only reached for patches nested deeper than @ref
detail::recursion_depth_limit.
*/
void merge_patch_iteratively(const basic_json& apply_patch)
void merge_patch_iteratively(basic_json& apply_patch)
{
std::vector<merge_frame> stack;
// patch `target` with `patch`, or start patching it member by member
const auto apply = [&stack](basic_json & target, const basic_json & patch)
const auto apply = [&stack](basic_json & target, basic_json & patch)
{
if (patch.is_object())
{
@@ -6628,11 +6663,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
{
target = basic_json::object();
}
stack.emplace_back(&target, patch.cbegin(), patch.cend());
stack.emplace_back(&target, patch.begin(), patch.end());
}
else
{
target = patch;
target = std::move(patch);
}
};
@@ -6648,7 +6683,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
continue;
}
const const_iterator member = frame.position;
const iterator member = frame.position;
++stack.back().position;
if (member.value().is_null())
{

View File

@@ -31162,25 +31162,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/update/
void update(const_reference j, bool merge_objects = false)
{
update(j.begin(), j.end(), merge_objects);
prepare_update();
// passed value must be an object (checked here so a type_error names
// j, not the copy made below)
if (JSON_HEDLEY_UNLIKELY(!j.is_object()))
{
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", j.type_name()), &j));
}
// copy first: j may be *this or one of its descendants, and is
// iterated (and moved from) below to update *this
basic_json source = j;
update_from(source, merge_objects);
}
/// @brief updates a JSON object from another object, overwriting existing keys
/// @sa https://json.nlohmann.me/api/basic_json/update/
void update(const_iterator first, const_iterator last, bool merge_objects = false) // NOLINT(performance-unnecessary-value-param)
{
// implicitly convert a null value to an empty object
if (is_null())
{
m_data.m_type = value_t::object;
m_data.m_value.object = create<object_t>();
assert_invariant();
}
if (JSON_HEDLEY_UNLIKELY(!is_object()))
{
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this));
}
prepare_update();
// check if range iterators belong to the same JSON object
if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object))
@@ -31194,7 +31195,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", first.m_object->type_name()), first.m_object));
}
update_members(first, last, merge_objects, 0);
// copy first: the range may belong to *this or one of its
// descendants, and is iterated (and moved from) below to update
// *this
basic_json source(first, last);
update_from(source, merge_objects);
}
private:
@@ -31202,15 +31207,42 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// merge_patch_iteratively is merging into, and the members still to merge
struct merge_frame
{
merge_frame(basic_json* target_, const_iterator position_, const_iterator last_) noexcept
merge_frame(basic_json* target_, iterator position_, iterator last_) noexcept
: target(target_), position(std::move(position_)), last(std::move(last_))
{}
basic_json* target;
const_iterator position;
const_iterator last;
iterator position;
iterator last;
};
/// @brief converts a null value to an empty object and checks that this
/// value is an object; called first by both @ref update overloads
void prepare_update()
{
// implicitly convert a null value to an empty object; create the
// object before setting the type, so a throwing allocation leaves
// this value null
if (is_null())
{
m_data.m_value.object = create<object_t>();
m_data.m_type = value_t::object;
assert_invariant();
}
if (JSON_HEDLEY_UNLIKELY(!is_object()))
{
JSON_THROW(type_error::create(312, detail::concat("cannot use update() with ", type_name()), this));
}
}
/// @brief starts the @ref update_members loop over an already-copied @a
/// source; called by both @ref update overloads
void update_from(basic_json& source, const bool merge_objects)
{
update_members(source.begin(), source.end(), merge_objects, 0);
}
/*!
@brief the members loop of @ref update, for this object and range
@@ -31223,7 +31255,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
@param[in] depth nesting level of this object, counted from the object
@ref update was called on
*/
void update_members(const const_iterator& first, const const_iterator& last, const bool merge_objects, const std::size_t depth)
void update_members(const iterator& first, const iterator& last, const bool merge_objects, const std::size_t depth)
{
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
@@ -31241,13 +31273,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// are overwritten as usual" behavior (see #5402).
if (it2 != m_data.m_value.object->end() && it2->second.is_object())
{
it2->second.update_members(it.value().cbegin(), it.value().cend(), true, depth + 1);
it2->second.update_members(it.value().begin(), it.value().end(), true, depth + 1);
continue;
}
}
// set_parent() also repairs the other members, which ordered_json
// relocates when adding a key makes its vector grow
set_parent(m_data.m_value.object->operator[](it.key()) = it.value());
set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value()));
}
}
@@ -31261,7 +31293,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
version. Only reached for values nested deeper than @ref
detail::recursion_depth_limit.
*/
void update_members_iteratively(const_iterator first, const_iterator last)
void update_members_iteratively(iterator first, iterator last)
{
std::vector<merge_frame> stack;
@@ -31288,18 +31320,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const auto it2 = target->m_data.m_value.object->find(first.key());
if (it2 != target->m_data.m_value.object->end() && it2->second.is_object())
{
const basic_json& source = first.value();
basic_json& source = first.value();
++first;
stack.emplace_back(target, first, last);
target = &it2->second;
first = source.cbegin();
last = source.cend();
first = source.begin();
last = source.end();
continue;
}
}
// set_parent() also repairs the other members, which ordered_json
// relocates when adding a key makes its vector grow
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = first.value());
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value()));
++first;
}
}
@@ -33308,7 +33340,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/merge_patch/
void merge_patch(const basic_json& apply_patch)
{
apply_merge_patch(apply_patch, 0);
// copy first: apply_patch may be *this or one of its descendants,
// and is iterated (and moved from) below to patch *this
basic_json patch = apply_patch;
apply_merge_patch(patch, 0);
}
private:
@@ -33321,7 +33356,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::recursion_depth_limit levels have been entered, @ref
merge_patch_iteratively applies what is left without the call stack.
*/
void apply_merge_patch(const basic_json& apply_patch, const std::size_t depth)
void apply_merge_patch(basic_json& apply_patch, const std::size_t depth)
{
if (apply_patch.is_object())
{
@@ -33349,7 +33384,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
else
{
*this = apply_patch;
*this = std::move(apply_patch);
}
}
@@ -33362,12 +33397,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
recursive version. Only reached for patches nested deeper than @ref
detail::recursion_depth_limit.
*/
void merge_patch_iteratively(const basic_json& apply_patch)
void merge_patch_iteratively(basic_json& apply_patch)
{
std::vector<merge_frame> stack;
// patch `target` with `patch`, or start patching it member by member
const auto apply = [&stack](basic_json & target, const basic_json & patch)
const auto apply = [&stack](basic_json & target, basic_json & patch)
{
if (patch.is_object())
{
@@ -33375,11 +33410,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
{
target = basic_json::object();
}
stack.emplace_back(&target, patch.cbegin(), patch.cend());
stack.emplace_back(&target, patch.begin(), patch.end());
}
else
{
target = patch;
target = std::move(patch);
}
};
@@ -33395,7 +33430,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
continue;
}
const const_iterator member = frame.position;
const iterator member = frame.position;
++stack.back().position;
if (member.value().is_null())
{

View File

@@ -236,6 +236,31 @@ TEST_CASE("Regression tests for extended diagnostics")
}
}
SECTION("Regression test for issue #5641 - parent pointers after update()/merge_patch() with an aliasing argument")
{
// update()'s and merge_patch()'s argument may be *this or one of its
// descendants; the values moved out of the (temporary) copy must end
// up with their parent pointing at their new location in *this
{
json j = {{"a", {{"a", 1}, {"b", 2}}}};
j.update(j["a"]);
CHECK(j == json({{"a", 1}, {"b", 2}}));
// Must call operator[] on const element, otherwise m_parent gets updated.
auto const& constJ = j;
CHECK_THROWS_WITH_AS(constJ["a"].at(0), "[json.exception.type_error.304] (/a) cannot use at() with number", json::type_error);
}
{
json j = {{"a", {{"a", nullptr}, {"b", 2}}}};
j.merge_patch(j["a"]);
CHECK(j == json({{"b", 2}}));
auto const& constJ = j;
CHECK_THROWS_WITH_AS(constJ["b"].at(0), "[json.exception.type_error.304] (/b) cannot use at() with number", json::type_error);
}
}
SECTION("Regression test for issue #3032 - Yet another assertion failure when inserting into arrays with JSON_DIAGNOSTICS set")
{
// reference operator[](size_type idx)

View File

@@ -374,3 +374,52 @@ TEST_CASE("JSON Merge Patch and update on ordered_json")
CHECK(target == ordered_json::parse(R"({"a": 1, "e": {"y": 5}, "g": 6})"));
}
}
TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
{
SECTION("j.merge_patch(j): erasing a member destroys the node the loop's iterator points to")
{
// reproduces issue #5641, case 1
json j = {{"a", nullptr}, {"b", 1}};
j.merge_patch(j);
CHECK(j == json({{"b", 1}}));
}
SECTION("j.merge_patch(j[\"a\"]): removing \"a\" destroys the patch while it is iterated")
{
// reproduces issue #5641, case 2
json j = {{"a", {{"a", nullptr}, {"b", 2}}}};
j.merge_patch(j["a"]);
CHECK(j == json({{"b", 2}}));
}
SECTION("a patch nested past the iterative descent bound aliases *this")
{
// every depth on either side of where the iterative version takes
// over (detail::recursion_depth_limit(), 128); patching *this with
// itself is idempotent, aliased or not
for (const std::size_t depth :
{
std::size_t{0}, std::size_t{127}, std::size_t{128}, std::size_t{300}
})
{
CAPTURE(depth);
json j = json::parse(nested_objects(depth, 0));
const json expected = j;
j.merge_patch(j);
CHECK(j == expected);
}
}
SECTION("ordered_json")
{
using nlohmann::ordered_json;
SECTION("merge_patch with a member of *this")
{
ordered_json j = {{"a", {{"a", nullptr}, {"b", 2}}}};
j.merge_patch(j["a"]);
CHECK(j == ordered_json({{"b", 2}}));
}
}
}

View File

@@ -1116,3 +1116,72 @@ TEST_CASE("update() on deeply nested values")
CHECK(p->at("y") == 2);
}
}
TEST_CASE("update() with an argument that aliases *this (#5641)")
{
SECTION("the target is checked before the argument, as before the copy")
{
json j = 1;
CHECK_THROWS_WITH_AS(j.update(json::array()), "[json.exception.type_error.312] cannot use update() with number", json::type_error&);
CHECK_THROWS_WITH_AS(j.update(j.cbegin(), j.cend()), "[json.exception.type_error.312] cannot use update() with number", json::type_error&);
json k;
CHECK_THROWS_WITH_AS(k.update(json::array()), "[json.exception.type_error.312] cannot use update() with array", json::type_error&);
CHECK(k == json::object());
}
SECTION("const reference")
{
SECTION("j.update(j[\"a\"]): assigning into the argument's parent destroys it mid-iteration")
{
// reproduces issue #5641, case 3
json j = {{"a", {{"a", 1}, {"b", 2}}}};
j.update(j["a"]);
CHECK(j == json({{"a", 1}, {"b", 2}}));
}
SECTION("merge_objects with an argument that is a member of *this")
{
json j = {{"defaults", {{"opts", {{"a", 1}}}}}, {"opts", {{"b", 2}}}};
j.update(j["defaults"], true);
CHECK(j == json({{"defaults", {{"opts", {{"a", 1}}}}}, {"opts", {{"a", 1}, {"b", 2}}}}));
}
SECTION("ordered_json: inserting a new key relocates the vector behind the argument")
{
// reproduces issue #5641, case 4
using nlohmann::ordered_json;
ordered_json j = {{"a", {{"x", 1}, {"y", 2}, {"z", 3}}}};
j.update(j["a"]);
CHECK(j == ordered_json({{"a", {{"x", 1}, {"y", 2}, {"z", 3}}}, {"x", 1}, {"y", 2}, {"z", 3}}));
}
}
SECTION("iterator range")
{
SECTION("range that is a member of *this")
{
json j = {{"a", {{"a", 1}, {"b", 2}}}};
j.update(j["a"].begin(), j["a"].end());
CHECK(j == json({{"a", 1}, {"b", 2}}));
}
}
SECTION("nested past the iterative descent bound aliases *this")
{
// every depth on either side of where the iterative version takes
// over (detail::recursion_depth_limit(), 128); merging *this into
// itself is idempotent, aliased or not
for (const std::size_t depth :
{
std::size_t{0}, std::size_t{127}, std::size_t{128}, std::size_t{300}
})
{
CAPTURE(depth);
json j = json::parse(nested_objects(depth, 0));
const json expected = j;
j.update(j, true);
CHECK(j == expected);
}
}
}