Compare commits

..
Author SHA1 Message Date
Niels LohmannandClaude Sonnet 5 80475865c0 Fix std::terminate and null pointer access in input_stream_adapter
Parsing from a std::istream crashed in two unusual but valid stream
states, both in input_stream_adapter:

- With eofbit in the stream's exceptions() mask, get_character() sets
  eofbit via is->clear(), which throws std::ios_base::failure. While
  that exception unwinds, ~input_stream_adapter() called clear() again
  to reset eofbit, which is still set and still in the exception mask,
  so it throws a second time out of the (implicitly noexcept)
  destructor and std::terminate() is called. The destructor now only
  calls clear() if a bit other than eofbit remains set, so the first
  exception can propagate normally.
- For an std::istream without a stream buffer (rdbuf() == nullptr,
  e.g. std::istream(nullptr)), the constructor stored the null
  pointer without checking it, and get_character() dereferenced it.
  input_adapter(std::istream&) now throws parse_error.101 for such a
  stream, the same as it already does for a null FILE* or char*.

Added regression tests to unit-deserialization.cpp and, for the
JSON_PRECISE_STREAM_POSITION variant of get_character(), to
unit-precise-stream-position.cpp; both crashed before this fix.
Documented the two exceptions in parse.md and operator_gtgt.md.

Fixes #5646.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 23:40:41 +02:00
9 changed files with 102 additions and 38 deletions
+8 -1
View File
@@ -88,7 +88,12 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
## Exceptions
- Throws [`parse_error.101`](../../home/exceptions.md#jsonexceptionparse_error101) in case of an unexpected token, or
empty input like a null `FILE*` or `char*` pointer.
empty input like a null `FILE*` or `char*` pointer, or an `std::istream` without a stream buffer
(`#!cpp i.rdbuf() == nullptr`, for instance `#!cpp std::istream(nullptr)`).
- If reading from an `std::istream` reaches the end of the input and `eofbit` is part of the stream's
[`exceptions()`](https://en.cppreference.com/w/cpp/io/basic_ios/exceptions) mask, the `std::ios_base::failure`
thrown by the stream itself propagates instead of a `parse_error`, the same as it would for the standard library's
own extraction operators.
## Complexity
@@ -254,6 +259,8 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by
- Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0.
- `JSON_STRICT_NUL_HANDLING` added in version 3.13.0 to optionally reject a NUL byte in the input instead of treating
it as end of input; planned to become the default in version 4.0.0.
- Extended empty-input detection to also cover an `std::istream` without a stream buffer, and fixed a crash
(`std::terminate`) when parsing from an `std::istream` with `eofbit` in its exception mask, in version 3.13.0.
!!! warning "Deprecation"
+8 -1
View File
@@ -20,7 +20,12 @@ the stream `i`
## Exceptions
- Throws [`parse_error.101`](../home/exceptions.md#jsonexceptionparse_error101) in case of an unexpected token.
- Throws [`parse_error.101`](../home/exceptions.md#jsonexceptionparse_error101) in case of an unexpected token, or if
`i` has no stream buffer (`#!cpp i.rdbuf() == nullptr`, for instance `#!cpp std::istream(nullptr)`).
- If reading from `i` reaches the end of the input and `eofbit` is part of `i`'s
[`exceptions()`](https://en.cppreference.com/w/cpp/io/basic_ios/exceptions) mask, the `std::ios_base::failure`
thrown by `i` itself propagates instead of a `parse_error`, the same as it would for the standard library's own
extraction operators.
## Complexity
@@ -118,3 +123,5 @@ being read.
it as end of input; planned to become the default in version 4.0.0.
- `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave the character that terminates a number in
the stream; planned to become the default in version 4.0.0.
- Fixed a null pointer dereference for an `std::istream` without a stream buffer (now throws `parse_error.101`), and a
crash (`std::terminate`) when `i` has `eofbit` in its exception mask, in version 3.13.0.
@@ -106,7 +106,13 @@ class input_stream_adapter
// was given back with release_lookahead()
commit_lookahead();
#endif
is->clear(is->rdstate() & std::ios::eofbit);
// only call clear() if there is something to clear: it throws
// std::ios_base::failure if the stream has exceptions() enabled
// for a state bit that remains set, and a destructor must not throw
if ((is->rdstate() & ~std::ios::eofbit) != 0)
{
is->clear(is->rdstate() & std::ios::eofbit);
}
}
}
@@ -811,12 +817,16 @@ inline file_input_adapter input_adapter(std::FILE* file)
inline input_stream_adapter input_adapter(std::istream& stream)
{
if (stream.rdbuf() == nullptr)
{
JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr));
}
return input_stream_adapter(stream);
}
inline input_stream_adapter input_adapter(std::istream&& stream)
{
return input_stream_adapter(stream);
return input_adapter(stream);
}
#endif // JSON_NO_IO
+1 -1
View File
@@ -336,7 +336,7 @@ void templated_json_throw(ExceptionType exception)
return ej_pair.second == j; \
}); \
if (it != std::end(m)) e = it->first; \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \
}
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
+13 -3
View File
@@ -2747,7 +2747,7 @@ void templated_json_throw(ExceptionType exception)
return ej_pair.second == j; \
}); \
if (it != std::end(m)) e = it->first; \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \
}
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
@@ -7650,7 +7650,13 @@ class input_stream_adapter
// was given back with release_lookahead()
commit_lookahead();
#endif
is->clear(is->rdstate() & std::ios::eofbit);
// only call clear() if there is something to clear: it throws
// std::ios_base::failure if the stream has exceptions() enabled
// for a state bit that remains set, and a destructor must not throw
if ((is->rdstate() & ~std::ios::eofbit) != 0)
{
is->clear(is->rdstate() & std::ios::eofbit);
}
}
}
@@ -8355,12 +8361,16 @@ inline file_input_adapter input_adapter(std::FILE* file)
inline input_stream_adapter input_adapter(std::istream& stream)
{
if (stream.rdbuf() == nullptr)
{
JSON_THROW(parse_error::create(101, 0, "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr));
}
return input_stream_adapter(stream);
}
inline input_stream_adapter input_adapter(std::istream&& stream)
{
return input_stream_adapter(stream);
return input_adapter(stream);
}
#endif // JSON_NO_IO
-24
View File
@@ -174,15 +174,6 @@ bool operator<(const char* op1, const alt_string& op2) noexcept
return op1 < op2.str_impl;
}
enum class alt_color { red, green };
// NOLINTNEXTLINE(misc-use-internal-linkage,misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(alt_color,
{
{alt_color::red, "red"},
{alt_color::green, "green"},
})
TEST_CASE("alternative string type")
{
SECTION("binary formats")
@@ -383,19 +374,4 @@ TEST_CASE("alternative string type")
const auto j2 = j.flatten();
CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})");
}
SECTION("strict enum")
{
// regression test for #5667: NLOHMANN_JSON_SERIALIZE_ENUM_STRICT's from_json
// built its exception message with "..." + j.dump(), which does not compile
// when j.dump() returns a custom string_t (here alt_string) instead of
// std::string
alt_json doc;
doc = "red";
CHECK(doc.get<alt_color>() == alt_color::red);
alt_json _;
doc = "blue";
CHECK_THROWS_WITH_AS(_ = doc.get<alt_color>(), "[json.exception.out_of_range.410] enum value out of range for alt_color: \"blue\"", alt_json::out_of_range&);
}
}
-6
View File
@@ -1740,12 +1740,6 @@ TEST_CASE("Strict JSON to enum mapping")
// conversion of unmapped enum -> exception thrown
CHECK_THROWS_WITH_AS(json(strict_cards::andere), "[json.exception.out_of_range.410] enum value out of range for strict_cards", json::out_of_range&);
// invalid UTF-8 -> out_of_range.410, not the type_error.316 thrown while building the
// message (regression test for #5667); such strings can reach get<Enum>() unvalidated,
// e.g. from from_cbor()/from_msgpack() (#5529)
const json j_invalid_utf8 = "\xFF";
CHECK_THROWS_WITH_AS(_ = j_invalid_utf8.get<strict_cards>(), "[json.exception.out_of_range.410] enum value out of range for strict_cards: \"\xEF\xBF\xBD\"", json::out_of_range&);
}
SECTION("traditional enum")
+31
View File
@@ -388,6 +388,37 @@ TEST_CASE("deserialization")
}));
}
SECTION("stream with eofbit in its exception mask (issue #5646)")
{
// reaching EOF while parsing a value that fills the whole input
// (e.g., a number, or any value under strict parsing) makes
// get_character() call std::istream::clear() to record eofbit;
// with eofbit in the exception mask, that clear() itself throws
// std::ios_base::failure - it must propagate to the caller instead
// of ~input_stream_adapter() throwing a second exception while the
// first is still unwinding, which would call std::terminate
json _;
std::istringstream is1("1");
is1.exceptions(std::ios::eofbit);
CHECK_THROWS_AS(_ = json::parse(is1), std::ios_base::failure&);
// the same holds for the common std::ifstream::exceptions(failbit |
// badbit | eofbit) pattern, because only eofbit ends up set
std::istringstream is2("1");
is2.exceptions(std::ios::failbit | std::ios::badbit | std::ios::eofbit);
CHECK_THROWS_AS(_ = json::parse(is2), std::ios_base::failure&);
}
SECTION("stream without a streambuf (issue #5646)")
{
// std::istream(nullptr) has badbit set and rdbuf() == nullptr;
// get_character() must not dereference that null streambuf
std::istream is(nullptr);
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
}
SECTION("string")
{
json::string_t const s = R"(["foo",1,2,3,false,{"one":1})";
@@ -234,4 +234,33 @@ TEST_CASE("JSON_PRECISE_STREAM_POSITION")
CHECK(j == json(1));
CHECK(remaining(is) == "true");
}
SECTION("stream with eofbit in its exception mask (issue #5646)")
{
// with JSON_PRECISE_STREAM_POSITION, get_character() peeks via
// sb->sgetc() rather than consuming via sb->sbumpc(), but it still
// calls std::istream::clear() to record eofbit once the streambuf is
// exhausted; with eofbit in the exception mask, that clear() itself
// throws std::ios_base::failure, which must propagate to the caller
// instead of ~input_stream_adapter() throwing a second exception
// while the first is still unwinding (which would call std::terminate)
json _;
std::istringstream is1("1");
is1.exceptions(std::ios::eofbit);
CHECK_THROWS_AS(_ = json::parse(is1), std::ios_base::failure&);
std::istringstream is2("1");
is2.exceptions(std::ios::failbit | std::ios::badbit | std::ios::eofbit);
CHECK_THROWS_AS(_ = json::parse(is2), std::ios_base::failure&);
}
SECTION("stream without a streambuf (issue #5646)")
{
// std::istream(nullptr) has badbit set and rdbuf() == nullptr;
// get_character() must not dereference that null streambuf
std::istream is(nullptr);
json _;
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
}
}