From 66717d184187a2fbf2415825b2f8becd989f9c25 Mon Sep 17 00:00:00 2001 From: FuzzTest Team Date: Sun, 1 Feb 2026 13:39:17 -0800 Subject: [PATCH] Add WithRepeatedFieldsSoftMaxSize to Protobuf domains. This new option allows users to disable the maximum size validation for repeated fields in Protobuf domains. When enabled, the fuzzer will not fail corpus value validation even if a repeated field exceeds the default or configured maximum size. PiperOrigin-RevId: 864040483 --- .../arbitrary_domains_protobuf_test.cc | 48 +++++++++++++++ fuzztest/internal/domains/container_of_impl.h | 21 +++++-- .../internal/domains/protobuf_domain_impl.h | 60 +++++++++++++++++-- 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/domain_tests/arbitrary_domains_protobuf_test.cc b/domain_tests/arbitrary_domains_protobuf_test.cc index 3028ac849..d04eca5d3 100644 --- a/domain_tests/arbitrary_domains_protobuf_test.cc +++ b/domain_tests/arbitrary_domains_protobuf_test.cc @@ -782,5 +782,53 @@ TEST(ProtocolBuffer, MutationInParallelIsEfficient) { std::cout << "ratio: " << multi_thread_time / single_thread_time << "\n"; } +TEST(ProtobufDomainTest, CorpusExceedingSoftMaxSizeIsValidWhileHardMaxIsNot) { + auto domain_with_soft_max_size = + Arbitrary().WithRepeatedFieldsSoftMaxSize(100); + auto domain_with_hard_max_size = + Arbitrary().WithRepeatedFieldsMaxSize(100); + TestProtobuf message; + for (int i = 0; i < 200; ++i) { + message.add_rep_i32(i); + } + auto corpus_with_soft_max_size = domain_with_soft_max_size.FromValue(message); + ASSERT_TRUE(corpus_with_soft_max_size.has_value()); + EXPECT_TRUE( + domain_with_soft_max_size.ValidateCorpusValue(*corpus_with_soft_max_size) + .ok()); + + auto corpus_with_hard_max_size = domain_with_hard_max_size.FromValue(message); + ASSERT_TRUE(corpus_with_hard_max_size.has_value()); + EXPECT_FALSE( + domain_with_hard_max_size.ValidateCorpusValue(*corpus_with_hard_max_size) + .ok()); +} + +TEST(ProtobufDomainTest, + NestedCorpusExceedingSoftMaxSizeIsValidAndMutationIsBounded) { + auto domain = Arbitrary().WithRepeatedFieldsSoftMaxSize(100); + TestProtobuf message; + for (int i = 0; i < 200; ++i) { + message.add_rep_i32(i); + } + auto* subproto = message.add_rep_subproto(); + for (int i = 0; i < 200; ++i) { + subproto->add_subproto_rep_i32(i); + } + auto corpus = domain.FromValue(message); + ASSERT_TRUE(corpus.has_value()); + EXPECT_TRUE(domain.ValidateCorpusValue(*corpus).ok()); + + absl::BitGen bitgen; + for (int i = 0; i < 1000; ++i) { + domain.Mutate(*corpus, bitgen, {}, /*only_shrink=*/false); + auto mutated_message = domain.GetValue(*corpus); + EXPECT_LE(mutated_message.rep_i32_size(), 200); + if (mutated_message.rep_subproto_size() > 0) { + EXPECT_LE(mutated_message.rep_subproto(0).subproto_rep_i32_size(), 200); + } + } +} + } // namespace } // namespace fuzztest diff --git a/fuzztest/internal/domains/container_of_impl.h b/fuzztest/internal/domains/container_of_impl.h index d4202b818..50ab0e24f 100644 --- a/fuzztest/internal/domains/container_of_impl.h +++ b/fuzztest/internal/domains/container_of_impl.h @@ -114,9 +114,15 @@ class ContainerOfImplBase const domain_implementor::MutationMetadata& metadata, bool only_shrink) { permanent_dict_candidate_ = std::nullopt; - FUZZTEST_CHECK(min_size() <= val.size() && val.size() <= max_size()) - << "Size " << val.size() << " is not between " << min_size() << " and " - << max_size(); + if (!validate_max_size()) { + FUZZTEST_CHECK(min_size() <= val.size()) + << "Size " << val.size() << " is smaller than min size " + << min_size(); + } else { + FUZZTEST_CHECK(min_size() <= val.size() && val.size() <= max_size()) + << "Size " << val.size() << " is not between " << min_size() + << " and " << max_size(); + } const bool can_shrink = val.size() > min_size(); const bool can_grow = !only_shrink && val.size() < max_size(); @@ -250,6 +256,10 @@ class ContainerOfImplBase manual_dict_provider_ = std::move(manual_dict_provider); return Self(); } + Derived& WithoutMaxSizeValidation() { + validate_max_size_ = false; + return Self(); + } auto GetPrinter() const { if constexpr (std::is_same_v || @@ -327,7 +337,7 @@ class ContainerOfImplBase return absl::InvalidArgumentError(absl::StrCat( "Invalid size: ", corpus_value.size(), ". Min size: ", min_size())); } - if (corpus_value.size() > max_size()) { + if (validate_max_size() && corpus_value.size() > max_size()) { return absl::InvalidArgumentError(absl::StrCat( "Invalid size: ", corpus_value.size(), ". Max size: ", max_size())); } @@ -355,6 +365,7 @@ class ContainerOfImplBase OtherInnerDomain>& other) { min_size_ = other.min_size_; max_size_ = other.max_size_; + validate_max_size_ = other.validate_max_size_; } protected: @@ -379,6 +390,7 @@ class ContainerOfImplBase size_t max_size() const { return max_size_.value_or(std::max(min_size_, kDefaultContainerMaxSize)); } + bool validate_max_size() const { return validate_max_size_; } private: Derived& Self() { return static_cast(*this); } @@ -395,6 +407,7 @@ class ContainerOfImplBase // DO NOT use directly. Use min_size() and max_size() instead. size_t min_size_ = 0; std::optional max_size_ = std::nullopt; + bool validate_max_size_ = true; // Temporary memory dictionary. Collected from tracing the program // execution. It will always be empty if no execution_coverage_ is found, diff --git a/fuzztest/internal/domains/protobuf_domain_impl.h b/fuzztest/internal/domains/protobuf_domain_impl.h index 789ff79a6..55cc75135 100644 --- a/fuzztest/internal/domains/protobuf_domain_impl.h +++ b/fuzztest/internal/domains/protobuf_domain_impl.h @@ -283,6 +283,11 @@ class ProtoPolicy { {/*filter=*/std::move(filter), /*value=*/max_size}); } + void DisableMaxSizeValidationForRepeatedFields(Filter filter) { + disable_max_size_validation_for_repeated_fields_.push_back( + {/*filter=*/std::move(filter), /*value=*/true}); + } + OptionalPolicy GetOptionalPolicy(const FieldDescriptor* field) const { FUZZTEST_CHECK(!field->is_required() && !field->is_repeated()) << "GetOptionalPolicy should apply to optional fields only!"; @@ -318,6 +323,15 @@ class ProtoPolicy { return max; } + bool ShouldValidateMaxSize(const FieldDescriptor* repeated_field) const { + FUZZTEST_CHECK(repeated_field->is_repeated()) + << "ShouldValidateMaxSize should apply to repeated " + "fields only!"; + return !GetPolicyValue(disable_max_size_validation_for_repeated_fields_, + repeated_field) + .value_or(false); + } + std::optional IsFieldFinitelyRecursive(const FieldDescriptor* field) { return caches_->IsFieldFinitelyRecursive(field); } @@ -490,6 +504,8 @@ class ProtoPolicy { std::vector> optional_policies_; std::vector> min_repeated_fields_sizes_; std::vector> max_repeated_fields_sizes_; + std::vector> + disable_max_size_validation_for_repeated_fields_; #define FUZZTEST_INTERNAL_POLICY_MEMBERS(Camel, cpp) \ private: \ @@ -918,6 +934,21 @@ class ProtobufDomainUntypedImpl return std::move(*this); } + ProtobufDomainUntypedImpl&& WithRepeatedFieldsSoftMaxSize( + int64_t max_size) && { + policy_.SetMaxRepeatedFieldsSize(IncludeAll(), max_size); + policy_.DisableMaxSizeValidationForRepeatedFields( + IncludeAll()); + return std::move(*this); + } + + ProtobufDomainUntypedImpl&& WithRepeatedFieldsSoftMaxSize( + std::function filter, int64_t max_size) && { + policy_.SetMaxRepeatedFieldsSize(filter, max_size); + policy_.DisableMaxSizeValidationForRepeatedFields(std::move(filter)); + return std::move(*this); + } + #define FUZZTEST_INTERNAL_WITH_FIELD(Camel, cpp, TAG) \ using Camel##type = MakeDependentType; \ ProtobufDomainUntypedImpl&& With##Camel##Fields( \ @@ -1626,7 +1657,8 @@ class ProtobufDomainUntypedImpl return ModifyDomainForRepeatedFieldRule( std::move(domain), use_policy ? policy_.GetMinRepeatedFieldSize(field) : std::nullopt, - use_policy ? policy_.GetMaxRepeatedFieldSize(field) : std::nullopt); + use_policy ? policy_.GetMaxRepeatedFieldSize(field) : std::nullopt, + use_policy ? policy_.ShouldValidateMaxSize(field) : true); } else if (IsRequired(field)) { return ModifyDomainForRequiredFieldRule(std::move(domain)); } else { @@ -1687,9 +1719,10 @@ class ProtobufDomainUntypedImpl // Simple wrapper that converts a Domain into a Domain>. template - static auto ModifyDomainForRepeatedFieldRule( - const Domain& d, std::optional min_size, - std::optional max_size) { + static auto ModifyDomainForRepeatedFieldRule(const Domain& d, + std::optional min_size, + std::optional max_size, + bool validate_max_size) { auto result = ContainerOfImpl, Domain>(d); if (min_size.has_value()) { result.WithMinSize(*min_size); @@ -1697,6 +1730,9 @@ class ProtobufDomainUntypedImpl if (max_size.has_value()) { result.WithMaxSize(*max_size); } + if (!validate_max_size) { + result.WithoutMaxSizeValidation(); + } return result; } @@ -2159,6 +2195,22 @@ class ProtobufDomainImpl return std::move(*this); } + ProtobufDomainImpl&& WithRepeatedFieldsSoftMaxSize(int64_t max_size) && { + inner_.GetPolicy().SetMaxRepeatedFieldsSize(IncludeAll(), + max_size); + inner_.GetPolicy().DisableMaxSizeValidationForRepeatedFields( + IncludeAll()); + return std::move(*this); + } + + ProtobufDomainImpl&& WithRepeatedFieldsSoftMaxSize( + std::function filter, int64_t max_size) && { + inner_.GetPolicy().SetMaxRepeatedFieldsSize(filter, max_size); + inner_.GetPolicy().DisableMaxSizeValidationForRepeatedFields( + std::move(filter)); + return std::move(*this); + } + ProtobufDomainImpl&& WithFieldUnset(absl::string_view field) && { inner_.WithFieldNullness(field, OptionalPolicy::kAlwaysNull); return std::move(*this);