From e0291678c10279c5cc288a9f22581fd4f73a01dd Mon Sep 17 00:00:00 2001 From: Shivendra Devadhe Date: Wed, 4 Mar 2026 17:36:53 +0530 Subject: [PATCH 1/6] feat(c++): add configurable deserialization size guardrails --- .../serialization/collection_serializer.h | 62 ++++++++++++++++++- .../collection_serializer_test.cc | 38 ++++++++++++ cpp/fory/serialization/config.h | 6 ++ cpp/fory/serialization/context.h | 6 ++ cpp/fory/serialization/fory.h | 12 ++++ cpp/fory/serialization/map_serializer.h | 14 +++++ cpp/fory/serialization/map_serializer_test.cc | 17 +++++ 7 files changed, 154 insertions(+), 1 deletion(-) diff --git a/cpp/fory/serialization/collection_serializer.h b/cpp/fory/serialization/collection_serializer.h index 3768275d14..0e6b782bde 100644 --- a/cpp/fory/serialization/collection_serializer.h +++ b/cpp/fory/serialization/collection_serializer.h @@ -392,6 +392,13 @@ inline void collection_insert(Container &result, T &&elem) { /// Read collection data for polymorphic or shared-ref elements. template inline Container read_collection_data_slow(ReadContext &ctx, uint32_t length) { + // Guardrail: Enforce max_collection_size for collection reads + if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); + return Container{}; + } + Container result; if constexpr (has_reserve_v) { result.reserve(length); @@ -611,15 +618,27 @@ struct Serializer< if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } + // 1. Guardrail: Enforce max_binary_size for binary byte-length reads + if (FORY_PREDICT_FALSE(total_bytes_u32 > ctx.config().max_binary_size)) { + ctx.set_error(Error::invalid_data("Binary size exceeds max_binary_size")); + return std::vector(); + } if (sizeof(T) == 0) { return std::vector(); } + + // 2. Convert bytes to element count and check max_collection_size + size_t elem_count = total_bytes_u32 / sizeof(T); + if (FORY_PREDICT_FALSE(elem_count > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::vector(); + } + if (total_bytes_u32 % sizeof(T) != 0) { ctx.set_error(Error::invalid_data( "Vector byte size not aligned with element size")); return std::vector(); } - size_t elem_count = total_bytes_u32 / sizeof(T); std::vector result(elem_count); if (total_bytes_u32 > 0) { ctx.read_bytes(result.data(), static_cast(total_bytes_u32), @@ -677,6 +696,12 @@ struct Serializer< if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } + + if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::vector(); + } + // Per xlang spec: header and type_info are omitted when length is 0 if (length == 0) { return std::vector(); @@ -971,6 +996,12 @@ template struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::list(); } + + if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::list(); + } + // Per xlang spec: header and type_info are omitted when length is 0 if (length == 0) { return std::list(); @@ -1161,6 +1192,12 @@ template struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::deque(); } + + if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::deque(); + } + // Per xlang spec: header and type_info are omitted when length is 0 if (length == 0) { return std::deque(); @@ -1814,6 +1851,12 @@ struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::set(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::set(); + } + // Per xlang spec: header and type_info are omitted when length is 0 if (size == 0) { return std::set(); @@ -1894,6 +1937,12 @@ struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::set(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::set(); + } + std::set result; for (uint32_t i = 0; i < size; ++i) { if (FORY_PREDICT_FALSE(ctx.has_error())) { @@ -1988,6 +2037,11 @@ struct Serializer> { return std::unordered_set(); } + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::unordered_set(); + } + // Per xlang spec: header and type_info are omitted when length is 0 if (size == 0) { return std::unordered_set(); @@ -2070,6 +2124,12 @@ struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::unordered_set(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + return std::unordered_set(); + } + std::unordered_set result; result.reserve(size); for (uint32_t i = 0; i < size; ++i) { diff --git a/cpp/fory/serialization/collection_serializer_test.cc b/cpp/fory/serialization/collection_serializer_test.cc index 0394ff2566..4591aac019 100644 --- a/cpp/fory/serialization/collection_serializer_test.cc +++ b/cpp/fory/serialization/collection_serializer_test.cc @@ -620,6 +620,44 @@ TEST(CollectionSerializerTest, ForwardListEmptyRoundTrip) { EXPECT_TRUE(deserialized.strings.empty()); } +TEST(CollectionSerializerTest, MaxCollectionSizeGuardrail) { + // Set a very small limit for testing + auto fory = Fory::builder().xlang(true).max_collection_size(2).build(); + fory.register_struct(201); + + VectorIntHolder original; + original.numbers = {1, 2, 3, 4, 5}; // Exceeds limit of 2 + + auto bytes_result = fory.serialize(original); + ASSERT_TRUE(bytes_result.ok()); + + // Deserialization should fail + auto deserialize_result = fory.deserialize( + bytes_result->data(), bytes_result->size()); + + ASSERT_FALSE(deserialize_result.ok()); + EXPECT_TRUE(deserialize_result.error().message().find( + "exceeds max_collection_size") != std::string::npos); +} + +TEST(CollectionSerializerTest, MaxBinarySizeGuardrail) { + // Set a limit of 10 bytes + auto fory = Fory::builder().xlang(true).max_binary_size(10).build(); + + // Vector of int32_t (4 bytes each). 5 elements = 20 bytes total. + std::vector large_data = {1, 2, 3, 4, 5}; + + auto bytes_result = fory.serialize(large_data); + ASSERT_TRUE(bytes_result.ok()); + + auto deserialize_result = fory.deserialize>( + bytes_result->data(), bytes_result->size()); + + ASSERT_FALSE(deserialize_result.ok()); + EXPECT_TRUE(deserialize_result.error().message().find( + "exceeds max_binary_size") != std::string::npos); +} + } // namespace } // namespace serialization } // namespace fory diff --git a/cpp/fory/serialization/config.h b/cpp/fory/serialization/config.h index d471c39074..63062c7ee5 100644 --- a/cpp/fory/serialization/config.h +++ b/cpp/fory/serialization/config.h @@ -52,6 +52,12 @@ struct Config { /// When enabled, avoids duplicating shared objects and handles cycles. bool track_ref = true; + /// Maximum allowed size for binary data in bytes. + uint32_t max_binary_size = 64 * 1024 * 1024; // 64MB default + + /// Maximum allowed number of elements in a collection or entries in a map. + uint32_t max_collection_size = 1024 * 1024; // 1M elements default + /// Default constructor with sensible defaults Config() = default; }; diff --git a/cpp/fory/serialization/context.h b/cpp/fory/serialization/context.h index e080604f67..6a07cb9bdf 100644 --- a/cpp/fory/serialization/context.h +++ b/cpp/fory/serialization/context.h @@ -361,6 +361,9 @@ class WriteContext { /// reset context for reuse. void reset(); + /// get associated configuration. + inline const Config& config() const { return *config_; } + private: // Error state - accumulated during serialization, checked at the end Error error_; @@ -643,6 +646,9 @@ class ReadContext { /// reset context for reuse. void reset(); + /// get associated configuration. + inline const Config& config() const { return *config_; } + private: // Error state - accumulated during deserialization, checked at the end Error error_; diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h index 1c5f19522c..d618bc070e 100644 --- a/cpp/fory/serialization/fory.h +++ b/cpp/fory/serialization/fory.h @@ -123,6 +123,18 @@ class ForyBuilder { /// Build a thread-safe Fory instance (uses context pools). ThreadSafeFory build_thread_safe(); + /// Set the maximum allowed size for binary data in bytes. + inline ForyBuilder& max_binary_size(uint32_t size) { + config_.max_binary_size = size; + return *this; + } + + /// Set the maximum allowed number of elements in a collection or entries in a map. + inline ForyBuilder& max_collection_size(uint32_t size) { + config_.max_collection_size = size; + return *this; + } + private: Config config_; std::shared_ptr type_resolver_; diff --git a/cpp/fory/serialization/map_serializer.h b/cpp/fory/serialization/map_serializer.h index dd2952da99..ace9e297b3 100644 --- a/cpp/fory/serialization/map_serializer.h +++ b/cpp/fory/serialization/map_serializer.h @@ -551,6 +551,13 @@ inline MapType read_map_data_fast(ReadContext &ctx, uint32_t length) { static_assert(!is_shared_ref_v && !is_shared_ref_v, "Fast path is for non-shared-ref types only"); + // Guardrail: Enforce max_collection_size for map reads (entry count) + if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Map entry count exceeds max_collection_size")); + return MapType{}; + } + MapType result; MapReserver::reserve(result, length); @@ -682,6 +689,13 @@ inline MapType read_map_data_fast(ReadContext &ctx, uint32_t length) { /// Read map data for polymorphic or shared-ref maps template inline MapType read_map_data_slow(ReadContext &ctx, uint32_t length) { + // Guardrail: Enforce max_collection_size for map reads (entry count) + if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Map entry count exceeds max_collection_size")); + return MapType{}; + } + MapType result; MapReserver::reserve(result, length); diff --git a/cpp/fory/serialization/map_serializer_test.cc b/cpp/fory/serialization/map_serializer_test.cc index bf91e939f1..b98ec1782b 100644 --- a/cpp/fory/serialization/map_serializer_test.cc +++ b/cpp/fory/serialization/map_serializer_test.cc @@ -780,6 +780,23 @@ TEST(MapSerializerTest, LargeMapWithPolymorphicValues) { EXPECT_EQ(deserialized[299]->name, "value_y_299"); } +TEST(MapSerializerTest, MaxMapSizeGuardrail) { + // Limit to 2 entries + auto fory = Fory::builder().xlang(true).max_collection_size(2).build(); + + std::map large_map = {{"a", 1}, {"b", 2}, {"c", 3}}; + + auto serialize_result = fory.serialize(large_map); + ASSERT_TRUE(serialize_result.ok()); + + auto deserialize_result = fory.deserialize>( + serialize_result->data(), serialize_result->size()); + + ASSERT_FALSE(deserialize_result.ok()); + EXPECT_TRUE(deserialize_result.error().message().find( + "exceeds max_collection_size") != std::string::npos); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From 88e9197b586c45154506eea812663d7ddcc38463 Mon Sep 17 00:00:00 2001 From: Shivendra Devadhe Date: Wed, 4 Mar 2026 17:48:45 +0530 Subject: [PATCH 2/6] style: format C++ code with clang-format --- .../serialization/collection_serializer.h | 24 ++++++++++++------- cpp/fory/serialization/context.h | 4 ++-- cpp/fory/serialization/fory.h | 7 +++--- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/cpp/fory/serialization/collection_serializer.h b/cpp/fory/serialization/collection_serializer.h index 0e6b782bde..a8c638e0ee 100644 --- a/cpp/fory/serialization/collection_serializer.h +++ b/cpp/fory/serialization/collection_serializer.h @@ -630,7 +630,8 @@ struct Serializer< // 2. Convert bytes to element count and check max_collection_size size_t elem_count = total_bytes_u32 / sizeof(T); if (FORY_PREDICT_FALSE(elem_count > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::vector(); } @@ -698,7 +699,8 @@ struct Serializer< } if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::vector(); } @@ -998,7 +1000,8 @@ template struct Serializer> { } if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::list(); } @@ -1194,7 +1197,8 @@ template struct Serializer> { } if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::deque(); } @@ -1853,7 +1857,8 @@ struct Serializer> { } if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::set(); } @@ -1939,7 +1944,8 @@ struct Serializer> { } if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::set(); } @@ -2038,7 +2044,8 @@ struct Serializer> { } if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::unordered_set(); } @@ -2126,7 +2133,8 @@ struct Serializer> { } if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { - ctx.set_error(Error::invalid_data("Collection length exceeds max_collection_size")); + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); return std::unordered_set(); } diff --git a/cpp/fory/serialization/context.h b/cpp/fory/serialization/context.h index 6a07cb9bdf..2c4d532da8 100644 --- a/cpp/fory/serialization/context.h +++ b/cpp/fory/serialization/context.h @@ -362,7 +362,7 @@ class WriteContext { void reset(); /// get associated configuration. - inline const Config& config() const { return *config_; } + inline const Config &config() const { return *config_; } private: // Error state - accumulated during serialization, checked at the end @@ -647,7 +647,7 @@ class ReadContext { void reset(); /// get associated configuration. - inline const Config& config() const { return *config_; } + inline const Config &config() const { return *config_; } private: // Error state - accumulated during deserialization, checked at the end diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h index d618bc070e..25cd5ec255 100644 --- a/cpp/fory/serialization/fory.h +++ b/cpp/fory/serialization/fory.h @@ -124,13 +124,14 @@ class ForyBuilder { ThreadSafeFory build_thread_safe(); /// Set the maximum allowed size for binary data in bytes. - inline ForyBuilder& max_binary_size(uint32_t size) { + inline ForyBuilder &max_binary_size(uint32_t size) { config_.max_binary_size = size; return *this; } - /// Set the maximum allowed number of elements in a collection or entries in a map. - inline ForyBuilder& max_collection_size(uint32_t size) { + /// Set the maximum allowed number of elements in a collection or entries in a + /// map. + inline ForyBuilder &max_collection_size(uint32_t size) { config_.max_collection_size = size; return *this; } From 4b2814183591c44e41da4f592dbcda8b525789b3 Mon Sep 17 00:00:00 2001 From: Shivendra Devadhe Date: Fri, 6 Mar 2026 14:54:07 +0530 Subject: [PATCH 3/6] Updated the arithmetic vector path to use max_binary_size instead of max_collection_size --- .../serialization/collection_serializer.h | 8 +--- .../collection_serializer_test.cc | 38 ++++++++++--------- cpp/fory/serialization/map_serializer_test.cc | 9 +++-- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/cpp/fory/serialization/collection_serializer.h b/cpp/fory/serialization/collection_serializer.h index a8c638e0ee..8edeaa03a0 100644 --- a/cpp/fory/serialization/collection_serializer.h +++ b/cpp/fory/serialization/collection_serializer.h @@ -618,7 +618,7 @@ struct Serializer< if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } - // 1. Guardrail: Enforce max_binary_size for binary byte-length reads + // Guardrail: Enforce max_binary_size for binary byte-length reads if (FORY_PREDICT_FALSE(total_bytes_u32 > ctx.config().max_binary_size)) { ctx.set_error(Error::invalid_data("Binary size exceeds max_binary_size")); return std::vector(); @@ -627,13 +627,7 @@ struct Serializer< return std::vector(); } - // 2. Convert bytes to element count and check max_collection_size size_t elem_count = total_bytes_u32 / sizeof(T); - if (FORY_PREDICT_FALSE(elem_count > ctx.config().max_collection_size)) { - ctx.set_error( - Error::invalid_data("Collection length exceeds max_collection_size")); - return std::vector(); - } if (total_bytes_u32 % sizeof(T) != 0) { ctx.set_error(Error::invalid_data( diff --git a/cpp/fory/serialization/collection_serializer_test.cc b/cpp/fory/serialization/collection_serializer_test.cc index 4591aac019..31e660aaf1 100644 --- a/cpp/fory/serialization/collection_serializer_test.cc +++ b/cpp/fory/serialization/collection_serializer_test.cc @@ -620,42 +620,46 @@ TEST(CollectionSerializerTest, ForwardListEmptyRoundTrip) { EXPECT_TRUE(deserialized.strings.empty()); } +// Test max_collection_size using objects (e.g., strings) TEST(CollectionSerializerTest, MaxCollectionSizeGuardrail) { - // Set a very small limit for testing - auto fory = Fory::builder().xlang(true).max_collection_size(2).build(); - fory.register_struct(201); + auto fory = Fory::builder() + .xlang(true) + .max_collection_size(2) // Limit to 2 elements + .build(); + fory.register_struct(200); - VectorIntHolder original; - original.numbers = {1, 2, 3, 4, 5}; // Exceeds limit of 2 + VectorStringHolder original; + original.strings = {"one", "two", "three"}; // 3 elements > limit of 2 auto bytes_result = fory.serialize(original); ASSERT_TRUE(bytes_result.ok()); - // Deserialization should fail - auto deserialize_result = fory.deserialize( + auto deserialize_result = fory.deserialize( bytes_result->data(), bytes_result->size()); ASSERT_FALSE(deserialize_result.ok()); - EXPECT_TRUE(deserialize_result.error().message().find( - "exceeds max_collection_size") != std::string::npos); + EXPECT_TRUE(deserialize_result.error().message().find("exceeds max_collection_size") != std::string::npos); } +// Test max_binary_size using primitive numbers TEST(CollectionSerializerTest, MaxBinarySizeGuardrail) { - // Set a limit of 10 bytes - auto fory = Fory::builder().xlang(true).max_binary_size(10).build(); + auto fory = Fory::builder() + .xlang(true) + .max_binary_size(10) + .build(); + fory.register_struct(201); - // Vector of int32_t (4 bytes each). 5 elements = 20 bytes total. - std::vector large_data = {1, 2, 3, 4, 5}; + VectorIntHolder original; + original.numbers = {1, 2, 3, 4, 5}; - auto bytes_result = fory.serialize(large_data); + auto bytes_result = fory.serialize(original); ASSERT_TRUE(bytes_result.ok()); - auto deserialize_result = fory.deserialize>( + auto deserialize_result = fory.deserialize( bytes_result->data(), bytes_result->size()); ASSERT_FALSE(deserialize_result.ok()); - EXPECT_TRUE(deserialize_result.error().message().find( - "exceeds max_binary_size") != std::string::npos); + EXPECT_TRUE(deserialize_result.error().message().find("exceeds max_binary_size") != std::string::npos); } } // namespace diff --git a/cpp/fory/serialization/map_serializer_test.cc b/cpp/fory/serialization/map_serializer_test.cc index b98ec1782b..37fbe22a1c 100644 --- a/cpp/fory/serialization/map_serializer_test.cc +++ b/cpp/fory/serialization/map_serializer_test.cc @@ -781,8 +781,10 @@ TEST(MapSerializerTest, LargeMapWithPolymorphicValues) { } TEST(MapSerializerTest, MaxMapSizeGuardrail) { - // Limit to 2 entries - auto fory = Fory::builder().xlang(true).max_collection_size(2).build(); + auto fory = Fory::builder() + .xlang(true) + .max_collection_size(2) + .build(); std::map large_map = {{"a", 1}, {"b", 2}, {"c", 3}}; @@ -793,8 +795,7 @@ TEST(MapSerializerTest, MaxMapSizeGuardrail) { serialize_result->data(), serialize_result->size()); ASSERT_FALSE(deserialize_result.ok()); - EXPECT_TRUE(deserialize_result.error().message().find( - "exceeds max_collection_size") != std::string::npos); + EXPECT_TRUE(deserialize_result.error().message().find("exceeds max_collection_size") != std::string::npos); } int main(int argc, char **argv) { From 662800864f604d6b34620bcf1d525457bb2ecc90 Mon Sep 17 00:00:00 2001 From: Shivendra Devadhe Date: Fri, 6 Mar 2026 14:58:41 +0530 Subject: [PATCH 4/6] style: format C++ code with clang-format --- .../serialization/collection_serializer_test.cc | 17 ++++++++--------- cpp/fory/serialization/map_serializer_test.cc | 8 +++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/cpp/fory/serialization/collection_serializer_test.cc b/cpp/fory/serialization/collection_serializer_test.cc index 31e660aaf1..09f31563ab 100644 --- a/cpp/fory/serialization/collection_serializer_test.cc +++ b/cpp/fory/serialization/collection_serializer_test.cc @@ -623,9 +623,9 @@ TEST(CollectionSerializerTest, ForwardListEmptyRoundTrip) { // Test max_collection_size using objects (e.g., strings) TEST(CollectionSerializerTest, MaxCollectionSizeGuardrail) { auto fory = Fory::builder() - .xlang(true) - .max_collection_size(2) // Limit to 2 elements - .build(); + .xlang(true) + .max_collection_size(2) // Limit to 2 elements + .build(); fory.register_struct(200); VectorStringHolder original; @@ -638,15 +638,13 @@ TEST(CollectionSerializerTest, MaxCollectionSizeGuardrail) { bytes_result->data(), bytes_result->size()); ASSERT_FALSE(deserialize_result.ok()); - EXPECT_TRUE(deserialize_result.error().message().find("exceeds max_collection_size") != std::string::npos); + EXPECT_TRUE(deserialize_result.error().message().find( + "exceeds max_collection_size") != std::string::npos); } // Test max_binary_size using primitive numbers TEST(CollectionSerializerTest, MaxBinarySizeGuardrail) { - auto fory = Fory::builder() - .xlang(true) - .max_binary_size(10) - .build(); + auto fory = Fory::builder().xlang(true).max_binary_size(10).build(); fory.register_struct(201); VectorIntHolder original; @@ -659,7 +657,8 @@ TEST(CollectionSerializerTest, MaxBinarySizeGuardrail) { bytes_result->data(), bytes_result->size()); ASSERT_FALSE(deserialize_result.ok()); - EXPECT_TRUE(deserialize_result.error().message().find("exceeds max_binary_size") != std::string::npos); + EXPECT_TRUE(deserialize_result.error().message().find( + "exceeds max_binary_size") != std::string::npos); } } // namespace diff --git a/cpp/fory/serialization/map_serializer_test.cc b/cpp/fory/serialization/map_serializer_test.cc index 37fbe22a1c..6f27c17294 100644 --- a/cpp/fory/serialization/map_serializer_test.cc +++ b/cpp/fory/serialization/map_serializer_test.cc @@ -781,10 +781,7 @@ TEST(MapSerializerTest, LargeMapWithPolymorphicValues) { } TEST(MapSerializerTest, MaxMapSizeGuardrail) { - auto fory = Fory::builder() - .xlang(true) - .max_collection_size(2) - .build(); + auto fory = Fory::builder().xlang(true).max_collection_size(2).build(); std::map large_map = {{"a", 1}, {"b", 2}, {"c", 3}}; @@ -795,7 +792,8 @@ TEST(MapSerializerTest, MaxMapSizeGuardrail) { serialize_result->data(), serialize_result->size()); ASSERT_FALSE(deserialize_result.ok()); - EXPECT_TRUE(deserialize_result.error().message().find("exceeds max_collection_size") != std::string::npos); + EXPECT_TRUE(deserialize_result.error().message().find( + "exceeds max_collection_size") != std::string::npos); } int main(int argc, char **argv) { From db9d347295d46fa2047732b21ed551ee410bfb63 Mon Sep 17 00:00:00 2001 From: Shivendra Devadhe Date: Fri, 6 Mar 2026 16:17:49 +0530 Subject: [PATCH 5/6] refactor(c++): harden deserialization guardrails and expand native tests --- .../serialization/collection_serializer.h | 35 +++++++++++++++++++ .../collection_serializer_test.cc | 21 +++++------ cpp/fory/serialization/context.h | 3 -- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/cpp/fory/serialization/collection_serializer.h b/cpp/fory/serialization/collection_serializer.h index 8edeaa03a0..f40adeae26 100644 --- a/cpp/fory/serialization/collection_serializer.h +++ b/cpp/fory/serialization/collection_serializer.h @@ -829,6 +829,13 @@ struct Serializer< if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); + return std::vector(); + } + std::vector result; result.reserve(size); for (uint32_t i = 0; i < size; ++i) { @@ -1129,6 +1136,13 @@ template struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::list(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); + return std::list(); + } + std::list result; for (uint32_t i = 0; i < size; ++i) { if (FORY_PREDICT_FALSE(ctx.has_error())) { @@ -1326,6 +1340,13 @@ template struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::deque(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); + return std::deque(); + } + std::deque result; for (uint32_t i = 0; i < size; ++i) { if (FORY_PREDICT_FALSE(ctx.has_error())) { @@ -1387,6 +1408,13 @@ struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::forward_list(); } + + if (FORY_PREDICT_FALSE(length > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); + return std::forward_list(); + } + // Per xlang spec: header and type_info are omitted when length is 0 if (length == 0) { return std::forward_list(); @@ -1751,6 +1779,13 @@ struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::forward_list(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_collection_size)) { + ctx.set_error( + Error::invalid_data("Collection length exceeds max_collection_size")); + return std::forward_list(); + } + std::vector temp; temp.reserve(size); for (uint32_t i = 0; i < size; ++i) { diff --git a/cpp/fory/serialization/collection_serializer_test.cc b/cpp/fory/serialization/collection_serializer_test.cc index 09f31563ab..e50afa2901 100644 --- a/cpp/fory/serialization/collection_serializer_test.cc +++ b/cpp/fory/serialization/collection_serializer_test.cc @@ -621,15 +621,12 @@ TEST(CollectionSerializerTest, ForwardListEmptyRoundTrip) { } // Test max_collection_size using objects (e.g., strings) -TEST(CollectionSerializerTest, MaxCollectionSizeGuardrail) { - auto fory = Fory::builder() - .xlang(true) - .max_collection_size(2) // Limit to 2 elements - .build(); +TEST(CollectionSerializerTest, MaxCollectionSizeNativeGuardrail) { + auto fory = Fory::builder().xlang(false).max_collection_size(2).build(); fory.register_struct(200); VectorStringHolder original; - original.strings = {"one", "two", "three"}; // 3 elements > limit of 2 + original.strings = {"A", "B", "C"}; auto bytes_result = fory.serialize(original); ASSERT_TRUE(bytes_result.ok()); @@ -643,17 +640,15 @@ TEST(CollectionSerializerTest, MaxCollectionSizeGuardrail) { } // Test max_binary_size using primitive numbers -TEST(CollectionSerializerTest, MaxBinarySizeGuardrail) { - auto fory = Fory::builder().xlang(true).max_binary_size(10).build(); - fory.register_struct(201); +TEST(CollectionSerializerTest, MaxBinarySizeNativeGuardrail) { + auto fory = Fory::builder().xlang(false).max_binary_size(10).build(); - VectorIntHolder original; - original.numbers = {1, 2, 3, 4, 5}; + std::vector large_data = {1, 2, 3, 4, 5}; - auto bytes_result = fory.serialize(original); + auto bytes_result = fory.serialize(large_data); ASSERT_TRUE(bytes_result.ok()); - auto deserialize_result = fory.deserialize( + auto deserialize_result = fory.deserialize>( bytes_result->data(), bytes_result->size()); ASSERT_FALSE(deserialize_result.ok()); diff --git a/cpp/fory/serialization/context.h b/cpp/fory/serialization/context.h index 2c4d532da8..8fba3b6121 100644 --- a/cpp/fory/serialization/context.h +++ b/cpp/fory/serialization/context.h @@ -361,9 +361,6 @@ class WriteContext { /// reset context for reuse. void reset(); - /// get associated configuration. - inline const Config &config() const { return *config_; } - private: // Error state - accumulated during serialization, checked at the end Error error_; From 5e65bef6949ef1d5def74071743c4a3720a557d5 Mon Sep 17 00:00:00 2001 From: Shivendra Devadhe Date: Mon, 9 Mar 2026 22:15:00 +0530 Subject: [PATCH 6/6] refactor(c++): complete guardrail coverage for unsigned types and native paths --- .../serialization/collection_serializer.h | 6 +++++ cpp/fory/serialization/unsigned_serializer.h | 24 +++++++++++++++++++ .../serialization/unsigned_serializer_test.cc | 18 ++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/cpp/fory/serialization/collection_serializer.h b/cpp/fory/serialization/collection_serializer.h index f40adeae26..4193526855 100644 --- a/cpp/fory/serialization/collection_serializer.h +++ b/cpp/fory/serialization/collection_serializer.h @@ -925,6 +925,12 @@ template struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } + + if (FORY_PREDICT_FALSE(size > ctx.config().max_binary_size)) { + ctx.set_error(Error::invalid_data("Binary size exceeds max_binary_size")); + return std::vector(); + } + std::vector result(size); // Fast path: bulk read all bytes at once if we have enough buffer Buffer &buffer = ctx.buffer(); diff --git a/cpp/fory/serialization/unsigned_serializer.h b/cpp/fory/serialization/unsigned_serializer.h index e53ff250f2..5705b5ceb2 100644 --- a/cpp/fory/serialization/unsigned_serializer.h +++ b/cpp/fory/serialization/unsigned_serializer.h @@ -703,6 +703,12 @@ template <> struct Serializer> { static inline std::vector read_data(ReadContext &ctx) { uint32_t length = ctx.read_var_uint32(ctx.error()); + + if (FORY_PREDICT_FALSE(length > ctx.config().max_binary_size)) { + ctx.set_error(Error::invalid_data("Binary size exceeds max_binary_size")); + return std::vector(); + } + if (FORY_PREDICT_FALSE(length > ctx.buffer().remaining_size())) { ctx.set_error( Error::invalid_data("Invalid length: " + std::to_string(length))); @@ -798,6 +804,12 @@ template <> struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } + + if (FORY_PREDICT_FALSE(total_bytes > ctx.config().max_binary_size)) { + ctx.set_error(Error::invalid_data("Binary size exceeds max_binary_size")); + return std::vector(); + } + if (total_bytes % sizeof(uint16_t) != 0) { ctx.set_error(Error::invalid_data("Invalid length: " + std::to_string(total_bytes))); @@ -900,6 +912,12 @@ template <> struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } + + if (FORY_PREDICT_FALSE(total_bytes > ctx.config().max_binary_size)) { + ctx.set_error(Error::invalid_data("Binary size exceeds max_binary_size")); + return std::vector(); + } + if (total_bytes % sizeof(uint32_t) != 0) { ctx.set_error(Error::invalid_data("Invalid length: " + std::to_string(total_bytes))); @@ -1002,6 +1020,12 @@ template <> struct Serializer> { if (FORY_PREDICT_FALSE(ctx.has_error())) { return std::vector(); } + + if (FORY_PREDICT_FALSE(total_bytes > ctx.config().max_binary_size)) { + ctx.set_error(Error::invalid_data("Binary size exceeds max_binary_size")); + return std::vector(); + } + if (total_bytes % sizeof(uint64_t) != 0) { ctx.set_error(Error::invalid_data("Invalid length: " + std::to_string(total_bytes))); diff --git a/cpp/fory/serialization/unsigned_serializer_test.cc b/cpp/fory/serialization/unsigned_serializer_test.cc index 30196c1deb..75173515ed 100644 --- a/cpp/fory/serialization/unsigned_serializer_test.cc +++ b/cpp/fory/serialization/unsigned_serializer_test.cc @@ -271,6 +271,24 @@ TEST(UnsignedSerializerTest, UnsignedArrayTypeIdsAreDistinct) { static_cast(TypeId::BINARY)); } +TEST(UnsignedSerializerTest, MaxBinarySizeNativeGuardrail) { + // Set limit to 10 bytes + auto fory = Fory::builder().xlang(false).max_binary_size(10).build(); + + // 10 elements of uint32_t = 40 bytes > 10 byte limit + std::vector large_data(10, 42); + + auto bytes_result = fory.serialize(large_data); + ASSERT_TRUE(bytes_result.ok()); + + auto deserialize_result = fory.deserialize>( + bytes_result->data(), bytes_result->size()); + + ASSERT_FALSE(deserialize_result.ok()); + EXPECT_TRUE(deserialize_result.error().message().find( + "exceeds max_binary_size") != std::string::npos); +} + } // namespace test } // namespace serialization } // namespace fory