From 57890cebadb602d0734977ecf6839aa21ebc68d9 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 29 Sep 2026 23:41:00 +0200 Subject: [PATCH] Fix value(json_pointer, default) aborting under JSON_NOEXCEPTION With exceptions disabled (JSON_NOEXCEPTION or -fno-exceptions), value(const json_pointer&, default) called std::abort() for array reference tokens that array_index() rejects with out_of_range.404/410: indices too large to fit size_type, the empty token ("/"), and tokens like "/1a". With exceptions enabled, the same tokens correctly yielded the default value, because get_checked_or_null() relied on JSON_TRY/JSON_INTERNAL_CATCH (detail::out_of_range&) to turn the exception into nullptr; under JSON_NOEXCEPTION, JSON_THROW aborts before that catch is ever reached. get_checked_or_null() now detects those out-of-range tokens itself, the same way contains(json_pointer) already does (#5495), and only calls array_index() for tokens that must still raise parse_error.106 or parse_error.109 (e.g. "/01", "/+1"), matching the documented behavior of value(). Added regression tests to tests/src/unit-disabled_exceptions.cpp (built with JSON_NOEXCEPTION and -fno-exceptions) and the matching checks to tests/src/unit-element_access2.cpp for normal exception mode. Fixes #5672. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 26 ++++++++++++++++-------- single_include/nlohmann/json.hpp | 26 ++++++++++++++++-------- tests/src/unit-disabled_exceptions.cpp | 15 ++++++++++++++ tests/src/unit-element_access2.cpp | 15 ++++++++++++++ 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 1540a8d6f..63f579d81 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -678,19 +678,29 @@ class json_pointer return nullptr; } - // may throw parse_error.106/109 for a malformed index; an - // index that is syntactically valid but cannot be - // represented (out_of_range.404/410) is treated like an - // out-of-range index below - typename BasicJsonType::size_type idx{}; - JSON_TRY + // tokens that array_index() rejects with parse_error.106/109 + // are passed on to it; all other tokens that it would reject + // with out_of_range.404/410 are detected here, so that this + // also works without exceptions + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 && !(reference_token[0] >= '1' && reference_token[0] <= '9'))) { - idx = array_index(reference_token); + static_cast(array_index(reference_token)); // throws parse_error.106/109 } - JSON_INTERNAL_CATCH (detail::out_of_range&) + if (JSON_HEDLEY_UNLIKELY(reference_token.empty() || !std::all_of(reference_token.begin(), reference_token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }))) { return nullptr; } + errno = 0; // strtoull() does not reset errno on success + char* p_end = nullptr; // NOLINT(misc-const-correctness) + const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(errno == ERANGE || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + { + return nullptr; + } + const auto idx = static_cast(magnitude); if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 576498738..0122a3905 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19520,19 +19520,29 @@ class json_pointer return nullptr; } - // may throw parse_error.106/109 for a malformed index; an - // index that is syntactically valid but cannot be - // represented (out_of_range.404/410) is treated like an - // out-of-range index below - typename BasicJsonType::size_type idx{}; - JSON_TRY + // tokens that array_index() rejects with parse_error.106/109 + // are passed on to it; all other tokens that it would reject + // with out_of_range.404/410 are detected here, so that this + // also works without exceptions + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 && !(reference_token[0] >= '1' && reference_token[0] <= '9'))) { - idx = array_index(reference_token); + static_cast(array_index(reference_token)); // throws parse_error.106/109 } - JSON_INTERNAL_CATCH (detail::out_of_range&) + if (JSON_HEDLEY_UNLIKELY(reference_token.empty() || !std::all_of(reference_token.begin(), reference_token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }))) { return nullptr; } + errno = 0; // strtoull() does not reset errno on success + char* p_end = nullptr; // NOLINT(misc-const-correctness) + const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(errno == ERANGE || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + { + return nullptr; + } + const auto idx = static_cast(magnitude); if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) { diff --git a/tests/src/unit-disabled_exceptions.cpp b/tests/src/unit-disabled_exceptions.cpp index e4532e234..100e0fbf1 100644 --- a/tests/src/unit-disabled_exceptions.cpp +++ b/tests/src/unit-disabled_exceptions.cpp @@ -46,6 +46,21 @@ TEST_CASE("Tests with disabled exceptions") CHECK(*sax_no_exception::error_string == "[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'x'"); delete sax_no_exception::error_string; // NOLINT(cppcoreguidelines-owning-memory) } + + SECTION("issue #5672 - value(json_pointer, default) must not abort for array tokens that are not a valid index") + { + const json j = {1, 2, 3}; + + // a syntactically valid index that is out of range for this array + CHECK(j.value("/7"_json_pointer, 42) == 42); + // a reference token that is not a number at all + CHECK(j.value("/1a"_json_pointer, 42) == 42); + // the empty reference token (JSON pointer "/") + CHECK(j.value("/"_json_pointer, 42) == 42); + // an index whose magnitude does not fit into size_type + CHECK(j.value("/99999999999999999999999"_json_pointer, 42) == 42); + CHECK(j.value("/18446744073709551615"_json_pointer, 42) == 42); + } } DOCTEST_GCC_SUPPRESS_WARNING_POP diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 40e8216d5..cd2ecd02d 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -482,6 +482,21 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_array.value("/-"_json_pointer, 42) == 42); CHECK(j_array_const.value("/-"_json_pointer, 42) == 42); + // Test an index with a non-digit after a valid leading digit; this is + // out_of_range (not parse_error) and must not throw (see #5672) + CHECK(j_array.value("/1a"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/1a"_json_pointer, 42) == 42); + + // Test the empty reference token (JSON pointer "/"); see #5672 + CHECK(j_array.value("/"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/"_json_pointer, 42) == 42); + + // Test an index whose magnitude does not fit into size_type (see #5672) + CHECK(j_array.value("/99999999999999999999999"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/99999999999999999999999"_json_pointer, 42) == 42); + CHECK(j_array.value("/18446744073709551615"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/18446744073709551615"_json_pointer, 42) == 42); + #if !defined(JSON_NOEXCEPTION) // Test malformed index (non-numeric) throws parse_error CHECK_THROWS_WITH_AS(j_array.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&);