From 5bd766aa505d0788422412c8d9360eb04a0e2120 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 20:07:41 +0200 Subject: [PATCH] Move to_bson's binary subtype check into calc_bson_sizes (#5703) to_bson() rejected a binary value's subtype above 255 (out_of_range.415) in write_bson_binary(), which only has the binary_t, not the basic_json value that holds it, so the exception was created with no JSON_DIAGNOSTICS context even though the equivalent to_msgpack() check names the value's path. The check also ran after the document size, all preceding elements, and this element's header and length had already reached the output adapter, so a caller-provided std::vector or std::string ended up holding a truncated document. calc_bson_sizes() already walks every value before anything is written, to size embedded documents and arrays and to reject invalid keys (out_of_range.409) up front. The subtype check now runs there instead, in calc_bson_binary_size(), which is given the basic_json value so the exception can use it as context. The now-redundant check in write_bson_binary() is removed, since calc_bson_sizes() always throws first if any binary value in the document has an oversized subtype. Fixes #5675. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/to_bson.md | 4 +++ .../nlohmann/detail/output/binary_writer.hpp | 26 +++++++++++++------ single_include/nlohmann/json.hpp | 26 +++++++++++++------ tests/src/unit-bson.cpp | 21 ++++++++++++++- tests/src/unit-diagnostics.cpp | 6 +++++ 5 files changed, 66 insertions(+), 17 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/to_bson.md b/docs/mkdocs/docs/api/basic_json/to_bson.md index fb02c51e1..5ba3c8bb4 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bson.md +++ b/docs/mkdocs/docs/api/basic_json/to_bson.md @@ -43,6 +43,9 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412) if the length of a document, array, string, or binary value exceeds the range of the 32-bit BSON length field; example: `"BSON length 2147483661 exceeds maximum of 2147483647"` +- Throws [`out_of_range.415`](../../home/exceptions.md#jsonexceptionout_of_range415) if the subtype of a binary value + exceeds 255, the maximum of the BSON binary subtype; example: + `"subtype 70000 is too large for the BSON binary subtype (max 255)"` ## Complexity @@ -78,3 +81,4 @@ pass before anything is written. - Added in version 3.4.0. - Linear in the size of `j`, and no longer limited by the call stack for deeply nested values, since version 3.13.0. +- `out_of_range.415` is now detected before anything is written, like the other exceptions above, since version 3.13.0. diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 4f39f0537..1ebd3c8ab 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -1056,15 +1056,26 @@ class binary_writer } /*! - @return The size of the BSON-encoded binary array @a value + @return The size of the BSON-encoded binary array in @a j + @throw out_of_range.415 if the subtype of @a j does not fit into a byte, + before anything is written */ - static std::size_t calc_bson_binary_size(const typename BasicJsonType::binary_t& value) + static std::size_t calc_bson_binary_size(const BasicJsonType& j) { + const auto& value = *j.m_data.m_value.binary; + + if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits::max)())) + { + JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), &j)); + } + return sizeof(std::int32_t) + value.size() + 1ul; } /*! @brief Writes a BSON element with key @a name and binary value @a value + @pre @a value's subtype, if any, fits into a byte; @ref calc_bson_sizes + checks this for every binary value in the document beforehand. */ void write_bson_binary(const string_t& name, const binary_t& value) @@ -1073,11 +1084,6 @@ class binary_writer write_number(to_bson_length(value.size()), true); - if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits::max)())) - { - JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), nullptr)); - } - write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); oa.write_characters(reinterpret_cast(value.data()), value.size()); @@ -1086,13 +1092,15 @@ class binary_writer /*! @return The size of the value of the BSON document entry for @a j, which is neither an object nor an array + @throw out_of_range.415 if @a j is binary with a subtype that does not fit + into a byte, before anything is written */ static std::size_t calc_bson_value_size(const BasicJsonType& j) { switch (j.type()) { case value_t::binary: - return calc_bson_binary_size(*j.m_data.m_value.binary); + return calc_bson_binary_size(j); case value_t::boolean: return 1ul; @@ -1218,6 +1226,8 @@ class binary_writer @return the size of @a document @throw out_of_range.409 if a key contains U+0000, before anything is written + @throw out_of_range.415 if a binary value's subtype does not fit into a + byte, before anything is written */ static std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector& nested_sizes) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4fadfb382..02065bdf8 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -22153,15 +22153,26 @@ class binary_writer } /*! - @return The size of the BSON-encoded binary array @a value + @return The size of the BSON-encoded binary array in @a j + @throw out_of_range.415 if the subtype of @a j does not fit into a byte, + before anything is written */ - static std::size_t calc_bson_binary_size(const typename BasicJsonType::binary_t& value) + static std::size_t calc_bson_binary_size(const BasicJsonType& j) { + const auto& value = *j.m_data.m_value.binary; + + if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits::max)())) + { + JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), &j)); + } + return sizeof(std::int32_t) + value.size() + 1ul; } /*! @brief Writes a BSON element with key @a name and binary value @a value + @pre @a value's subtype, if any, fits into a byte; @ref calc_bson_sizes + checks this for every binary value in the document beforehand. */ void write_bson_binary(const string_t& name, const binary_t& value) @@ -22170,11 +22181,6 @@ class binary_writer write_number(to_bson_length(value.size()), true); - if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits::max)())) - { - JSON_THROW(out_of_range::create(415, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), nullptr)); - } - write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); oa.write_characters(reinterpret_cast(value.data()), value.size()); @@ -22183,13 +22189,15 @@ class binary_writer /*! @return The size of the value of the BSON document entry for @a j, which is neither an object nor an array + @throw out_of_range.415 if @a j is binary with a subtype that does not fit + into a byte, before anything is written */ static std::size_t calc_bson_value_size(const BasicJsonType& j) { switch (j.type()) { case value_t::binary: - return calc_bson_binary_size(*j.m_data.m_value.binary); + return calc_bson_binary_size(j); case value_t::boolean: return 1ul; @@ -22315,6 +22323,8 @@ class binary_writer @return the size of @a document @throw out_of_range.409 if a key contains U+0000, before anything is written + @throw out_of_range.415 if a binary value's subtype does not fit into a + byte, before anything is written */ static std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector& nested_sizes) { diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 292ff4dc1..5bfaac7c2 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -797,7 +797,11 @@ TEST_CASE("regression test - BSON binary subtype rejects a value that doesn't fi CHECK(json::from_bson(json::to_bson(doc255))["b"].get_binary().subtype() == 255); CHECK_THROWS_AS(json::to_bson(json{{"b", json::binary({1, 2}, 256)}}), json::out_of_range); - CHECK_THROWS_WITH_AS(json::to_bson(json{{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range); +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bson(json {{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] (/b) subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range); +#else + CHECK_THROWS_WITH_AS(json::to_bson(json {{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.415] subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range); +#endif } TEST_CASE("BSON input/output_adapters") @@ -1805,6 +1809,21 @@ value = depth % 2 == 0 ? json{{"a", std::move(value)}, {"b", {1, "x"}}} : CHECK(output.empty()); } + SECTION("a binary subtype that doesn't fit a byte is rejected before anything is written (#5675)") + { + // the offending value is nested, so this also covers that the check + // is not limited to a directly written value's own document + json const j = {{"a", {{"b", json::binary({1, 2}, 300)}}}}; + + std::vector vector_output; + CHECK_THROWS_AS(json::to_bson(j, vector_output), json::out_of_range&); + CHECK(vector_output.empty()); + + std::string string_output; + CHECK_THROWS_AS(json::to_bson(j, string_output), json::out_of_range&); + CHECK(string_output.empty()); + } + SECTION("values nested too deeply for the call stack (#5392)") { // serializing recursed once per nesting level, and computed every diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp index a46ce5746..46f41f252 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -104,6 +104,12 @@ TEST_CASE("Regression tests for extended diagnostics") CHECK_THROWS_WITH_AS(j.unflatten(), "[json.exception.type_error.315] (/~1foo) values in object must be primitive", json::type_error); } + SECTION("Regression test for issue #5675 - to_bson: out_of_range.415 has no diagnostics context") + { + json const j = {{"a", {{"b", json::binary({1, 2}, 300)}}}}; + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.out_of_range.415] (/a/b) subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range); + } + SECTION("Regression test for issue #2838 - Assertion failure when inserting into arrays with JSON_DIAGNOSTICS set") { // void push_back(basic_json&& val)