From 389eb67fe958e90b58bd28adf0e3c7ef622f8a6e Mon Sep 17 00:00:00 2001 From: FuzzTest Team Date: Sun, 1 Feb 2026 13:39:17 -0800 Subject: [PATCH] Add WithRepeatedFieldsMaxSizeIgnored 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 --- domain_tests/BUILD | 1 + .../arbitrary_domains_protobuf_test.cc | 42 +++++++++++++++++ fuzztest/internal/domains/container_of_impl.h | 21 +++++++-- .../internal/domains/protobuf_domain_impl.h | 45 +++++++++++++++++-- 4 files changed, 101 insertions(+), 8 deletions(-) diff --git a/domain_tests/BUILD b/domain_tests/BUILD index 623ca0fbf..e65d621e3 100644 --- a/domain_tests/BUILD +++ b/domain_tests/BUILD @@ -73,6 +73,7 @@ cc_test( "@abseil-cpp//absl/time", "@com_google_fuzztest//fuzztest:domain", "@com_google_fuzztest//fuzztest/internal:test_protobuf_cc_proto", + "@com_google_fuzztest//fuzztest/internal/domains:core_domains_impl", "@googletest//:gtest_main", "@protobuf", ], diff --git a/domain_tests/arbitrary_domains_protobuf_test.cc b/domain_tests/arbitrary_domains_protobuf_test.cc index 3028ac849..affb91307 100644 --- a/domain_tests/arbitrary_domains_protobuf_test.cc +++ b/domain_tests/arbitrary_domains_protobuf_test.cc @@ -36,6 +36,7 @@ #include "absl/time/time.h" #include "./fuzztest/domain.h" // IWYU pragma: keep #include "./domain_tests/domain_testing.h" +#include "./fuzztest/internal/domains/container_of_impl.h" #include "./fuzztest/internal/test_protobuf.pb.h" #include "google/protobuf/descriptor.h" #include "google/protobuf/message.h" @@ -782,5 +783,46 @@ TEST(ProtocolBuffer, MutationInParallelIsEfficient) { std::cout << "ratio: " << multi_thread_time / single_thread_time << "\n"; } +TEST(ProtobufDomainTest, + WithRepeatedFieldsMaxSizeIgnoredMutationRespectsMaxSize) { + auto domain = Arbitrary().WithRepeatedFieldsMaxSizeIgnored(); + TestProtobuf message; + for (int i = 0; i < internal::kDefaultContainerMaxSize + 1; ++i) { + message.add_rep_i32(i); + } + auto corpus = domain.FromValue(message); + ASSERT_TRUE(corpus.has_value()); + EXPECT_OK(domain.ValidateCorpusValue(*corpus)); + + 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(), + internal::kDefaultContainerMaxSize + 1); + } +} + +TEST(ProtobufDomainTest, + WithRepeatedFieldsMaxSizeIgnoredMutationRespectsMaxSizeForNested) { + auto domain = Arbitrary().WithRepeatedFieldsMaxSizeIgnored(); + TestProtobuf message; + auto* subproto = message.add_rep_subproto(); + for (int i = 0; i < internal::kDefaultContainerMaxSize + 1; ++i) { + subproto->add_subproto_rep_i32(i); + } + auto corpus = domain.FromValue(message); + ASSERT_TRUE(corpus.has_value()); + absl::BitGen bitgen; + for (int i = 0; i < 1000; ++i) { + domain.Mutate(*corpus, bitgen, {}, /*only_shrink=*/false); + auto mutated_message = domain.GetValue(*corpus); + if (mutated_message.rep_subproto_size() > 0) { + EXPECT_LE(mutated_message.rep_subproto(0).subproto_rep_i32_size(), + internal::kDefaultContainerMaxSize + 1); + } + } +} + } // namespace } // namespace fuzztest diff --git a/fuzztest/internal/domains/container_of_impl.h b/fuzztest/internal/domains/container_of_impl.h index d4202b818..102e025b4 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 (skip_max_size_validation()) { + 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& SkipMaxSizeValidation() { + skip_max_size_validation_ = true; + 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 (!skip_max_size_validation() && 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_; + skip_max_size_validation_ = other.skip_max_size_validation_; } protected: @@ -379,6 +390,7 @@ class ContainerOfImplBase size_t max_size() const { return max_size_.value_or(std::max(min_size_, kDefaultContainerMaxSize)); } + bool skip_max_size_validation() const { return skip_max_size_validation_; } 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 skip_max_size_validation_ = false; // 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..326ae0a71 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 SetSkipMaxSizeValidationForRepeatedFields(Filter filter, bool skip) { + skip_max_size_validation_for_repeated_fields_.push_back( + {/*filter=*/std::move(filter), /*value=*/skip}); + } + 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 GetSkipMaxSizeValidationForRepeatedFields( + const FieldDescriptor* field) const { + FUZZTEST_CHECK(field->is_repeated()) + << "GetSkipMaxSizeValidationForRepeatedFields should apply to repeated " + "fields only!"; + return GetPolicyValue(skip_max_size_validation_for_repeated_fields_, 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> + skip_max_size_validation_for_repeated_fields_; #define FUZZTEST_INTERNAL_POLICY_MEMBERS(Camel, cpp) \ private: \ @@ -918,6 +934,13 @@ class ProtobufDomainUntypedImpl return std::move(*this); } + ProtobufDomainUntypedImpl&& WithRepeatedFieldsMaxSizeIgnored( + std::function filter = + IncludeAll()) && { + policy_.SetSkipMaxSizeValidationForRepeatedFields(std::move(filter), true); + return std::move(*this); + } + #define FUZZTEST_INTERNAL_WITH_FIELD(Camel, cpp, TAG) \ using Camel##type = MakeDependentType; \ ProtobufDomainUntypedImpl&& With##Camel##Fields( \ @@ -1626,7 +1649,9 @@ 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_.GetSkipMaxSizeValidationForRepeatedFields(field) + : false); } else if (IsRequired(field)) { return ModifyDomainForRequiredFieldRule(std::move(domain)); } else { @@ -1687,9 +1712,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 skip_max_size_validation) { auto result = ContainerOfImpl, Domain>(d); if (min_size.has_value()) { result.WithMinSize(*min_size); @@ -1697,6 +1723,9 @@ class ProtobufDomainUntypedImpl if (max_size.has_value()) { result.WithMaxSize(*max_size); } + if (skip_max_size_validation) { + result.SkipMaxSizeValidation(); + } return result; } @@ -2159,6 +2188,14 @@ class ProtobufDomainImpl return std::move(*this); } + ProtobufDomainImpl&& WithRepeatedFieldsMaxSizeIgnored( + std::function filter = + IncludeAll()) && { + inner_.GetPolicy().SetSkipMaxSizeValidationForRepeatedFields( + std::move(filter), true); + return std::move(*this); + } + ProtobufDomainImpl&& WithFieldUnset(absl::string_view field) && { inner_.WithFieldNullness(field, OptionalPolicy::kAlwaysNull); return std::move(*this);