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

+23
View File
@@ -1531,6 +1531,10 @@ class basic_json_document
/// sentinel: src[size] is readable and 0 (std::string, C strings)
void build(const char* src, std::size_t size, on_error allow_exceptions, bool comments, bool trailing_commas, bool owned, bool sentinel)
{
// precondition of read(): a borrowed input is not held by this document,
// which releases what it holds below (checked in debug builds only, as
// it would cost every read() of a small message a few percent)
NLOHMANN_VIEW_ASSERT(owned || !m_data || !m_data->holds(src, size));
ensure_data(src, size);
document_data& d = *m_data;
if (!owned)
@@ -1620,6 +1624,10 @@ class basic_json_document
template<typename T>
void read_kind(const T& s, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::copy_range> /*unused*/)
{
// (the precondition of read() as for a borrowed input, although only an
// input in the owned text breaks a copy: the same call borrows, for
// example a std::string_view in C++17, and must not depend on that)
NLOHMANN_VIEW_ASSERT(!m_data || !m_data->holds(reinterpret_cast<const char*>(s.data()), static_cast<std::size_t>(s.size()))); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
build_copy(s, ae, c, tc);
}
@@ -1638,9 +1646,24 @@ class basic_json_document
template<typename T>
void read_kind(T&& input, on_error ae, bool c, bool tc, std::integral_constant<input_kind, input_kind::adapter> /*unused*/)
{
NLOHMANN_VIEW_ASSERT(!held(input)); // (see the copy_range overload)
build_copy(std::forward<T>(input), ae, c, tc);
}
/// whether an input read through the input adapter is held by this document:
/// string_view_t before C++17 (a string handed out by a view), for the
/// debug check of read(); other inputs of this kind do not point into it
bool held(const detail::view::string_ref& s) const noexcept
{
return m_data && m_data->holds(s.data(), s.size());
}
template<typename T>
bool held(const T& /*input*/) const noexcept
{
return false;
}
template<typename IteratorType>
void read_range(IteratorType first, IteratorType last, on_error ae, bool c, bool tc)
{