Document that read() must not be given a view of the document itself (#6086)

* Document that read() must not be given a view of the document itself

basic_json_document::read() releases the previous text, the decoded
strings and the edits before it parses its input. An input that points
into them (source(), get_string(), number_token(), or a string of an
edited document) is read after its memory was cleared or freed.

A guard that copies such input costs about 1 ns per read() (5 % of a
read of a 7-byte input), which this API exists to avoid, so state the
precondition in read.md, lifetime.md and the pages of the views that
hand out such strings instead, and show how to copy the string first.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Assert in debug builds that read() is not given its own text

read() releases the document's text, decoded strings, owned image and
edits before it parses, so an input that points into them is read after
release. Without NDEBUG, read() now asserts that a borrowed input, a
copied contiguous input and (before C++17) a string_ref input are not
held by the document: its owned text, its decoded strings, an owned
image, and the current buffer of edited strings. Release builds keep
the documented precondition without a check, which would cost 2-4.5 %
per read() of a small message.

JSON_ASSERT is undefined at the end of json.hpp, so the view uses
assert() through a new internal NLOHMANN_VIEW_ASSERT.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-11-11 22:36:17 +01:00
1 parent a7b377e9ec
commit 94331d1050
12 files changed
+215 -5

No files matched your search

@@ -12,6 +12,7 @@
#include <cstddef> // size_t
#include <cstdint> // uint8_t, uint32_t
#include <cstring> // memcmp, memcpy
#include <functional> // less
#include <limits> // numeric_limits
#include <map> // map
#include <memory> // unique_ptr
@@ -149,6 +150,23 @@ struct document_data
node_capacity = inline_capacity;
}
/// whether the n bytes at p overlap memory that the next parse releases or
/// reuses: the owned text, the decoded strings, an owned image, and the
/// current buffer of strings written by edits (earlier edit buffers are not
/// covered, as their sizes are not kept); for the debug check in read()
bool holds(const char* p, std::size_t n) const noexcept
{
const auto overlaps = [p, n](const char* b, std::size_t m) noexcept
{
// (std::less: a total order also for pointers into different objects)
return n != 0 && m != 0 && std::less<const char*>()(p, b + m) && std::less<const char*>()(b, p + n);
};
return overlaps(owned.data(), owned.capacity())
|| overlaps(arena.data(), arena.capacity())
|| overlaps(reinterpret_cast<const char*>(owned_image.data()), owned_image.capacity()) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|| (edits && !edits->texts.empty() && overlaps(edits->texts.back().get(), edits->text_cap));
}
/// the largest node count whose size in bytes fits a std::size_t
static constexpr std::size_t max_nodes() noexcept
{
@@ -71,6 +71,11 @@
#define NLOHMANN_VIEW_THROW JSON_THROW_USER
#endif
// debug checks of preconditions (assert(), off with NDEBUG): JSON_ASSERT is
// one of the macros that json.hpp undefines at its end
#include <cassert> // assert
#define NLOHMANN_VIEW_ASSERT(x) assert(x)
// the parser stores a node's first word at once where the layout of `node` is
// known to be little-endian (MSVC targets are); elsewhere field by field
#if (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || defined(_MSC_VER)
@@ -18,6 +18,7 @@
#undef NLOHMANN_VIEW_NOCLONE
#undef NLOHMANN_VIEW_NODISCARD
#undef NLOHMANN_VIEW_THROW
#undef NLOHMANN_VIEW_ASSERT
#undef NLOHMANN_VIEW_LITTLE_ENDIAN
#undef NLOHMANN_VIEW_REPEAT16
#undef NLOHMANN_VIEW_REPEAT_SECOND_8