Compare commits

..
Author SHA1 Message Date
Niels Lohmann c194c7703d Assert that update() and merge_patch() are not called with *this
Passing *this itself is cheap to detect; a value contained in *this is not. Document the assertion. Addresses review comment by @gregmarr.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-30 07:33:15 +02:00
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
11 changed files with 125 additions and 23 deletions
+1 -1
View File
@@ -10,5 +10,5 @@ title: "JSON for Modern C++"
version: 3.12.0
date-released: 2025-04-07
license: MIT
repository-code: "https://github.com/nlohmann/json"
repository-code: "https://github.com/nlohmann"
url: https://json.nlohmann.me
+1 -4
View File
@@ -42,11 +42,8 @@ endif()
## OPTIONS
##
# Build the tests by default only for the main project and only if the tests
# directory exists (the release archive json.tar.xz does not contain it).
# VERSION_GREATER_EQUAL is not available in older CMake (< 3.7)
if(${MAIN_PROJECT} AND (${CMAKE_VERSION} VERSION_EQUAL 3.13 OR ${CMAKE_VERSION} VERSION_GREATER 3.13)
AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/CMakeLists.txt")
if(${MAIN_PROJECT} AND (${CMAKE_VERSION} VERSION_EQUAL 3.13 OR ${CMAKE_VERSION} VERSION_GREATER 3.13))
set(JSON_BuildTests_INIT ON)
else()
set(JSON_BuildTests_INIT OFF)
+2 -5
View File
@@ -7,9 +7,6 @@
# find GNU sed to use `-i` parameter
SED:=$(shell command -v gsed || which sed)
# find GNU tar to use `--sort` and `--pax-option` parameters
TAR:=$(shell command -v gtar || which tar)
##########################################################################
# source files
@@ -218,8 +215,8 @@ ChangeLog.md:
# archive is created according to the advices of <https://reproducible-builds.org/docs/archives/>.
json.tar.xz:
mkdir json
rsync -R $(shell find LICENSE.MIT nlohmann_json.natvis CMakeLists.txt cmake/*.in include single_include src/modules -type f) json
$(TAR) --sort=name --mtime="@$(shell git log -1 --pretty=%ct)" --owner=0 --group=0 --numeric-owner --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime --create --file - json | xz --compress -9e --threads=2 - > json.tar.xz
rsync -R $(shell find LICENSE.MIT nlohmann_json.natvis CMakeLists.txt cmake/*.in include single_include -type f) json
gtar --sort=name --mtime="@$(shell git log -1 --pretty=%ct)" --owner=0 --group=0 --numeric-owner --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime --create --file - json | xz --compress -9e --threads=2 - > json.tar.xz
rm -fr json
# We use `-X` to make the resulting ZIP file reproducible, see
+9 -12
View File
@@ -1204,18 +1204,15 @@ language bindings, format converters, and the like. See the curated [Ecosystem](
Though it's 2026 already, the support for C++11 is still a bit sparse. Currently, the following compilers are known to work:
- GCC 4.8 - 16.2 (and possibly later)
- Clang 3.4 - 22.1 (and possibly later)
- Apple Clang 15.0 - 21.0 (and possibly later)
- Intel C++ Compiler Classic (icpc) 2021.10
- Intel oneAPI DPC++/C++ Compiler (icpx) 2025.3 (and possibly later)
- NVIDIA CUDA Compiler (nvcc) 11.8 - 12.6 (and possibly later)
- NVIDIA HPC SDK C++ Compiler (nvc++) 25.5 (and possibly later)
- Microsoft Visual C++ 2015 / MSVC 19.0 (and possibly later)
- Microsoft Visual C++ 2017 / MSVC 19.16 (and possibly later)
- Microsoft Visual C++ 2019 / MSVC 19.29 (and possibly later)
- Microsoft Visual C++ 2022 / MSVC 19.44 (and possibly later)
- Microsoft Visual C++ 2026 / MSVC 19.51 (and possibly later)
- GCC 4.8 - 14.2 (and possibly later)
- Clang 3.4 - 21.0 (and possibly later)
- Apple Clang 9.1 - 16.0 (and possibly later)
- Intel C++ Compiler 17.0.2 (and possibly later)
- Nvidia CUDA Compiler 11.0.221 (and possibly later)
- Microsoft Visual C++ 2015 / Build Tools 14.0.25123.0 (and possibly later)
- Microsoft Visual C++ 2017 / Build Tools 15.5.180.51428 (and possibly later)
- Microsoft Visual C++ 2019 / Build Tools 16.3.1+1def00d3d (and possibly later)
- Microsoft Visual C++ 2022 / Build Tools 19.30.30709.0 (and possibly later)
I would be happy to learn about other compilers/versions.
@@ -37,6 +37,25 @@ 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 and runtime assertions"
`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)
```
Passing `#!cpp *this` itself is **guarded by a [runtime assertion](../../features/assertions.md)**; a value
contained in `#!cpp *this` is not detected.
See [GitHub issue #5641](https://github.com/nlohmann/json/issues/5641) for more information.
## Examples
??? example
@@ -61,3 +80,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.
+21
View File
@@ -59,6 +59,25 @@ 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 and runtime assertions"
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"])
```
Passing `#!cpp *this` itself is **guarded by a [runtime assertion](../../features/assertions.md)**; a value
contained in `#!cpp *this` is not detected.
See [GitHub issue #5641](https://github.com/nlohmann/json/issues/5641) for more information.
## Examples
??? example
@@ -155,3 +174,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.
+30
View File
@@ -103,6 +103,36 @@ behavior and yields a runtime assertion.
Assertion failed: (m_object != nullptr), function operator++, file iter_impl.hpp, line 368.
```
### Updating or merge-patching a value with itself
Functions [`update`](../api/basic_json/update.md) and [`merge_patch`](../api/basic_json/merge_patch.md) read their
argument while they modify the value they are called on. Passing that value itself as the argument is undefined
behavior and yields a runtime assertion. Pass a copy instead, for example `#!cpp j.merge_patch(json(j))`. An argument
that refers to a value contained in the value (e.g., `#!cpp j.update(j["defaults"])`) is undefined behavior as well, but
is not detected.
??? example "Example 4: Merge-patching a value with itself"
The following code will trigger an assertion at runtime:
```cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
json j = {{"key", "value"}};
j.merge_patch(j);
}
```
Output:
```
Assertion failed: (&apply_patch != this), function merge_patch, file json.hpp, line 6310.
```
## Changes
### Reading from a null `FILE` or `char` pointer
+1 -1
View File
@@ -125,7 +125,7 @@ automatically download a release as a dependency at configure time.
### `JSON_BuildTests`
Build the unit tests when [`BUILD_TESTING`](https://cmake.org/cmake/help/latest/command/enable_testing.html) is enabled. This option is `ON` by default if the library's CMake project is the top project and the `tests` directory exists (the release archive `json.tar.xz` does not contain it). That is, when integrating the library as described above, the test suite is not built unless explicitly switched on with this option.
Build the unit tests when [`BUILD_TESTING`](https://cmake.org/cmake/help/latest/command/enable_testing.html) is enabled. This option is `ON` by default if the library's CMake project is the top project. That is, when integrating the library as described above, the test suite is not built unless explicitly switched on with this option.
### `JSON_CI`
+6
View File
@@ -4218,6 +4218,9 @@ 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));
}
// the range must not be *this; a range inside *this is not detected
JSON_ASSERT(first.m_object != this);
update_members(first, last, merge_objects, 0);
}
@@ -6303,6 +6306,9 @@ 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)
{
// the patch must not be *this; a patch inside *this is not detected
JSON_ASSERT(&apply_patch != this);
apply_merge_patch(apply_patch, 0);
}
+6
View File
@@ -30299,6 +30299,9 @@ 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));
}
// the range must not be *this; a range inside *this is not detected
JSON_ASSERT(first.m_object != this);
update_members(first, last, merge_objects, 0);
}
@@ -32384,6 +32387,9 @@ 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)
{
// the patch must not be *this; a patch inside *this is not detected
JSON_ASSERT(&apply_patch != this);
apply_merge_patch(apply_patch, 0);
}
+27
View File
@@ -41,6 +41,33 @@ TEST_CASE("JSON_ASSERT(x)")
// check that assertion actually happened
CHECK(assert_counter == 1);
}
SECTION("update() and merge_patch() with *this")
{
// update() and merge_patch() must not be called with *this (#5641);
// the values are chosen so the calls still happen to work without
// aborting, and only the assertion counter is checked
json j = {{"a", 1}};
assert_counter = 0;
j.update(j);
CHECK(assert_counter == 1);
assert_counter = 0;
j.update(j.cbegin(), j.cend());
CHECK(assert_counter == 1);
assert_counter = 0;
j.merge_patch(j);
CHECK(assert_counter == 1);
// a copy is fine
assert_counter = 0;
j.update(json(j));
j.merge_patch(json(j));
CHECK(assert_counter == 0);
CHECK(j == json({{"a", 1}}));
}
}
#endif