From 125b642419099847ed7fca4098dc0d194a85feff Mon Sep 17 00:00:00 2001 From: Alastair Donaldson Date: Tue, 22 Sep 2020 10:07:58 +0100 Subject: [PATCH] spirv-fuzz: Refactor fuzzer, replayer and shrinker (#3818) In preparation for some upcoming work on the shrinker, this PR changes the interfaces of Fuzzer, Replayer and Shrinker so that all data relevant to each class is provided on construction, meaning that the "Run" method can become a zero-argument method that returns a status, transformed binary and sequence of applied transformations via a struct. This makes greater use of fields, so that -- especially in Fuzzer -- there is a lot less parameter passing. --- source/fuzz/fuzzer.cpp | 369 ++++++++++------------------- source/fuzz/fuzzer.h | 108 +++++---- source/fuzz/replayer.cpp | 70 +++--- source/fuzz/replayer.h | 66 ++++-- source/fuzz/shrinker.cpp | 143 ++++++----- source/fuzz/shrinker.h | 62 +++-- test/fuzz/fuzzer_replayer_test.cpp | 46 ++-- test/fuzz/fuzzer_shrinker_test.cpp | 53 ++--- test/fuzz/replayer_test.cpp | 72 +++--- tools/fuzz/fuzz.cpp | 70 +++--- 10 files changed, 506 insertions(+), 553 deletions(-) diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp index 79228db06..845c8c043 100644 --- a/source/fuzz/fuzzer.cpp +++ b/source/fuzz/fuzzer.cpp @@ -88,7 +88,6 @@ #include "source/fuzz/pass_management/repeated_pass_manager_simple.h" #include "source/fuzz/pass_management/repeated_pass_recommender_standard.h" #include "source/fuzz/protobufs/spirvfuzz_protobufs.h" -#include "source/fuzz/pseudo_random_generator.h" #include "source/fuzz/transformation_context.h" #include "source/opt/build_module.h" #include "source/spirv_fuzzer_options.h" @@ -104,59 +103,59 @@ const uint32_t kTransformationLimit = 2000; } // namespace -Fuzzer::Fuzzer(spv_target_env target_env, uint32_t seed, bool enable_all_passes, +Fuzzer::Fuzzer(spv_target_env target_env, MessageConsumer consumer, + const std::vector& binary_in, + const protobufs::FactSequence& initial_facts, + const std::vector& donor_suppliers, + std::unique_ptr random_generator, + bool enable_all_passes, RepeatedPassStrategy repeated_pass_strategy, bool validate_after_each_fuzzer_pass, spv_validator_options validator_options) : target_env_(target_env), - seed_(seed), + consumer_(std::move(consumer)), + binary_in_(binary_in), + initial_facts_(initial_facts), + donor_suppliers_(donor_suppliers), + random_generator_(std::move(random_generator)), enable_all_passes_(enable_all_passes), repeated_pass_strategy_(repeated_pass_strategy), validate_after_each_fuzzer_pass_(validate_after_each_fuzzer_pass), validator_options_(validator_options), - num_repeated_passes_applied_(0) {} + num_repeated_passes_applied_(0), + ir_context_(nullptr), + fuzzer_context_(nullptr), + transformation_context_(nullptr), + transformation_sequence_out_() {} Fuzzer::~Fuzzer() = default; -void Fuzzer::SetMessageConsumer(MessageConsumer consumer) { - consumer_ = std::move(consumer); -} - template -void Fuzzer::MaybeAddRepeatedPass( - RepeatedPassInstances* pass_instances, opt::IRContext* ir_context, - TransformationContext* transformation_context, - FuzzerContext* fuzzer_context, - protobufs::TransformationSequence* transformation_sequence_out, - Args&&... extra_args) const { - if (enable_all_passes_ || fuzzer_context->ChooseEven()) { +void Fuzzer::MaybeAddRepeatedPass(RepeatedPassInstances* pass_instances, + Args&&... extra_args) { + if (enable_all_passes_ || fuzzer_context_->ChooseEven()) { pass_instances->SetPass(MakeUnique( - ir_context, transformation_context, fuzzer_context, - transformation_sequence_out, std::forward(extra_args)...)); + ir_context_.get(), transformation_context_.get(), fuzzer_context_.get(), + &transformation_sequence_out_, std::forward(extra_args)...)); } } template -void Fuzzer::MaybeAddFinalPass( - std::vector>* passes, - opt::IRContext* ir_context, TransformationContext* transformation_context, - FuzzerContext* fuzzer_context, - protobufs::TransformationSequence* transformation_sequence_out, - Args&&... extra_args) const { - if (enable_all_passes_ || fuzzer_context->ChooseEven()) { +void Fuzzer::MaybeAddFinalPass(std::vector>* passes, + Args&&... extra_args) { + if (enable_all_passes_ || fuzzer_context_->ChooseEven()) { passes->push_back(MakeUnique( - ir_context, transformation_context, fuzzer_context, - transformation_sequence_out, std::forward(extra_args)...)); + ir_context_.get(), transformation_context_.get(), fuzzer_context_.get(), + &transformation_sequence_out_, std::forward(extra_args)...)); } } bool Fuzzer::ApplyPassAndCheckValidity( - FuzzerPass* pass, const opt::IRContext& ir_context, - const spvtools::SpirvTools& tools) const { + FuzzerPass* pass, const spvtools::SpirvTools& tools) const { pass->Apply(); if (validate_after_each_fuzzer_pass_) { std::vector binary_to_validate; - ir_context.module()->ToBinary(&binary_to_validate, false); + ir_context_->module()->ToBinary(&binary_to_validate, false); if (!tools.Validate(&binary_to_validate[0], binary_to_validate.size(), validator_options_)) { consumer_(SPV_MSG_INFO, nullptr, {}, @@ -168,38 +167,37 @@ bool Fuzzer::ApplyPassAndCheckValidity( return true; } -Fuzzer::FuzzerResultStatus Fuzzer::Run( - const std::vector& binary_in, - const protobufs::FactSequence& initial_facts, - const std::vector& donor_suppliers, - std::vector* binary_out, - protobufs::TransformationSequence* transformation_sequence_out) { +Fuzzer::FuzzerResult Fuzzer::Run() { // Check compatibility between the library version being linked with and the // header files being used. GOOGLE_PROTOBUF_VERIFY_VERSION; + assert(ir_context_ == nullptr && fuzzer_context_ == nullptr && + transformation_context_ == nullptr && + transformation_sequence_out_.transformation_size() == 0 && + "'Run' must not be invoked more than once."); + spvtools::SpirvTools tools(target_env_); tools.SetMessageConsumer(consumer_); if (!tools.IsValid()) { consumer_(SPV_MSG_ERROR, nullptr, {}, "Failed to create SPIRV-Tools interface; stopping."); - return Fuzzer::FuzzerResultStatus::kFailedToCreateSpirvToolsInterface; + return {Fuzzer::FuzzerResultStatus::kFailedToCreateSpirvToolsInterface, + std::vector(), protobufs::TransformationSequence()}; } // Initial binary should be valid. - if (!tools.Validate(&binary_in[0], binary_in.size(), validator_options_)) { + if (!tools.Validate(&binary_in_[0], binary_in_.size(), validator_options_)) { consumer_(SPV_MSG_ERROR, nullptr, {}, "Initial binary is invalid; stopping."); - return Fuzzer::FuzzerResultStatus::kInitialBinaryInvalid; + return {Fuzzer::FuzzerResultStatus::kInitialBinaryInvalid, + std::vector(), protobufs::TransformationSequence()}; } // Build the module from the input binary. - std::unique_ptr ir_context = - BuildModule(target_env_, consumer_, binary_in.data(), binary_in.size()); - assert(ir_context); - - // Make a PRNG from the seed passed to the fuzzer on creation. - PseudoRandomGenerator random_generator(seed_); + ir_context_ = + BuildModule(target_env_, consumer_, binary_in_.data(), binary_in_.size()); + assert(ir_context_); // The fuzzer will introduce new ids into the module. The module's id bound // gives the smallest id that can be used for this purpose. We add an offset @@ -208,13 +206,14 @@ Fuzzer::FuzzerResultStatus Fuzzer::Run( // // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/2541) consider the // case where the maximum id bound is reached. - auto minimum_fresh_id = ir_context->module()->id_bound() + kIdBoundGap; - FuzzerContext fuzzer_context(&random_generator, minimum_fresh_id); + auto minimum_fresh_id = ir_context_->module()->id_bound() + kIdBoundGap; + fuzzer_context_ = + MakeUnique(random_generator_.get(), minimum_fresh_id); FactManager fact_manager; - fact_manager.AddFacts(consumer_, initial_facts, ir_context.get()); - TransformationContext transformation_context(&fact_manager, - validator_options_); + fact_manager.AddFacts(consumer_, initial_facts_, ir_context_.get()); + transformation_context_ = + MakeUnique(&fact_manager, validator_options_); RepeatedPassInstances pass_instances{}; do { @@ -223,243 +222,133 @@ Fuzzer::FuzzerResultStatus Fuzzer::Run( // if it is enabled. // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3764): Consider // enabling some passes always, or with higher probability. - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out, donor_suppliers); + &pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances, + donor_suppliers_); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); + MaybeAddRepeatedPass(&pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); + MaybeAddRepeatedPass(&pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); - MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); + MaybeAddRepeatedPass(&pass_instances); + MaybeAddRepeatedPass(&pass_instances); MaybeAddRepeatedPass( - &pass_instances, ir_context.get(), &transformation_context, - &fuzzer_context, transformation_sequence_out); + &pass_instances); // There is a theoretical possibility that no pass instances were created // until now; loop again if so. } while (pass_instances.GetPasses().empty()); RepeatedPassRecommenderStandard pass_recommender(&pass_instances, - &fuzzer_context); + fuzzer_context_.get()); std::unique_ptr repeated_pass_manager = nullptr; switch (repeated_pass_strategy_) { case RepeatedPassStrategy::kSimple: repeated_pass_manager = MakeUnique( - &fuzzer_context, &pass_instances); + fuzzer_context_.get(), &pass_instances); break; case RepeatedPassStrategy::kLoopedWithRecommendations: repeated_pass_manager = MakeUnique( - &fuzzer_context, &pass_instances, &pass_recommender); + fuzzer_context_.get(), &pass_instances, &pass_recommender); break; case RepeatedPassStrategy::kRandomWithRecommendations: repeated_pass_manager = MakeUnique( - &fuzzer_context, &pass_instances, &pass_recommender); + fuzzer_context_.get(), &pass_instances, &pass_recommender); break; } do { if (!ApplyPassAndCheckValidity(repeated_pass_manager->ChoosePass(), - *ir_context, tools)) { - return Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule; + tools)) { + return {Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule, + std::vector(), protobufs::TransformationSequence()}; } - } while ( - ShouldContinueFuzzing(*transformation_sequence_out, &fuzzer_context)); + } while (ShouldContinueFuzzing()); // Now apply some passes that it does not make sense to apply repeatedly, // as they do not unlock other passes. std::vector> final_passes; - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); - MaybeAddFinalPass( - &final_passes, ir_context.get(), &transformation_context, &fuzzer_context, - transformation_sequence_out); + &final_passes); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); + MaybeAddFinalPass(&final_passes); for (auto& pass : final_passes) { - if (!ApplyPassAndCheckValidity(pass.get(), *ir_context, tools)) { - return Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule; + if (!ApplyPassAndCheckValidity(pass.get(), tools)) { + return {Fuzzer::FuzzerResultStatus::kFuzzerPassLedToInvalidModule, + std::vector(), protobufs::TransformationSequence()}; } } - // Encode the module as a binary. - ir_context->module()->ToBinary(binary_out, false); + std::vector binary_out; + ir_context_->module()->ToBinary(&binary_out, false); - return Fuzzer::FuzzerResultStatus::kComplete; + return {Fuzzer::FuzzerResultStatus::kComplete, std::move(binary_out), + std::move(transformation_sequence_out_)}; } -bool Fuzzer::ShouldContinueFuzzing( - const protobufs::TransformationSequence& transformation_sequence_out, - FuzzerContext* fuzzer_context) { +bool Fuzzer::ShouldContinueFuzzing() { // There's a risk that fuzzing could get stuck, if none of the enabled fuzzer // passes are able to apply any transformations. To guard against this we // count the number of times some repeated pass has been applied and ensure @@ -473,7 +362,7 @@ bool Fuzzer::ShouldContinueFuzzing( return false; } auto transformations_applied_so_far = - static_cast(transformation_sequence_out.transformation_size()); + static_cast(transformation_sequence_out_.transformation_size()); if (transformations_applied_so_far >= kTransformationLimit) { // Stop because we have reached the transformation limit. return false; @@ -481,7 +370,7 @@ bool Fuzzer::ShouldContinueFuzzing( auto chance_of_continuing = static_cast( 100.0 * (1.0 - (static_cast(transformations_applied_so_far) / static_cast(kTransformationLimit)))); - if (!fuzzer_context->ChoosePercentage(chance_of_continuing)) { + if (!fuzzer_context_->ChoosePercentage(chance_of_continuing)) { // We have probabilistically decided to stop. return false; } diff --git a/source/fuzz/fuzzer.h b/source/fuzz/fuzzer.h index 01b2c68ec..379c0415d 100644 --- a/source/fuzz/fuzzer.h +++ b/source/fuzz/fuzzer.h @@ -24,6 +24,8 @@ #include "source/fuzz/pass_management/repeated_pass_instances.h" #include "source/fuzz/pass_management/repeated_pass_recommender.h" #include "source/fuzz/protobufs/spirvfuzz_protobufs.h" +#include "source/fuzz/random_generator.h" +#include "source/opt/ir_context.h" #include "spirv-tools/libspirv.hpp" namespace spvtools { @@ -41,6 +43,12 @@ class Fuzzer { kInitialBinaryInvalid, }; + struct FuzzerResult { + FuzzerResultStatus status; + std::vector transformed_binary; + protobufs::TransformationSequence applied_transformations; + }; + // Each field of this enum corresponds to an available repeated pass // strategy, and is used to decide which kind of RepeatedPassManager object // to create. @@ -50,15 +58,12 @@ class Fuzzer { kLoopedWithRecommendations }; - // Constructs a fuzzer from the given target environment |target_env|. |seed| - // is a seed for pseudo-random number generation. If |enable_all_passes| is - // true then all fuzzer passes will be enabled, otherwise a random subset of - // fuzzer passes will be enabled. |validate_after_each_fuzzer_pass| controls - // whether the validator will be invoked after every fuzzer pass is applied, - // and |validator_options| provides the options that should be used during - // validation if so. - Fuzzer(spv_target_env target_env, uint32_t seed, bool enable_all_passes, - RepeatedPassStrategy repeated_pass_strategy, + Fuzzer(spv_target_env target_env, MessageConsumer consumer, + const std::vector& binary_in, + const protobufs::FactSequence& initial_facts, + const std::vector& donor_suppliers, + std::unique_ptr random_generator, + bool enable_all_passes, RepeatedPassStrategy repeated_pass_strategy, bool validate_after_each_fuzzer_pass, spv_validator_options validator_options); @@ -70,73 +75,66 @@ class Fuzzer { ~Fuzzer(); - // Sets the message consumer to the given |consumer|. The |consumer| will be - // invoked once for each message communicated from the library. - void SetMessageConsumer(MessageConsumer consumer); - - // Transforms |binary_in| to |binary_out| by running a number of randomized - // fuzzer passes. Initial facts about the input binary and the context in - // which it will execute are provided via |initial_facts|. A source of donor - // modules to be used by transformations is provided via |donor_suppliers|. - // The transformation sequence that was applied is returned via - // |transformation_sequence_out|. - FuzzerResultStatus Run( - const std::vector& binary_in, - const protobufs::FactSequence& initial_facts, - const std::vector& donor_suppliers, - std::vector* binary_out, - protobufs::TransformationSequence* transformation_sequence_out); + // Transforms |binary_in_| by running a number of randomized fuzzer passes. + // Initial facts about the input binary and the context in which it will + // execute are provided via |initial_facts_|. A source of donor modules to be + // used by transformations is provided via |donor_suppliers_|. On success, + // returns a successful result status together with the transformed binary and + // the sequence of transformations that were applied. Otherwise, returns an + // appropriate result status together with an empty binary and empty + // transformation sequence. + FuzzerResult Run(); private: // A convenience method to add a repeated fuzzer pass to |pass_instances| with // probability 0.5, or with probability 1 if |enable_all_passes_| is true. // - // All fuzzer passes take |ir_context|, |transformation_context|, - // |fuzzer_context| and |transformation_sequence_out| as parameters. Extra + // All fuzzer passes take members |ir_context_|, |transformation_context_|, + // |fuzzer_context_| and |transformation_sequence_out_| as parameters. Extra // arguments can be provided via |extra_args|. template - void MaybeAddRepeatedPass( - RepeatedPassInstances* pass_instances, opt::IRContext* ir_context, - TransformationContext* transformation_context, - FuzzerContext* fuzzer_context, - protobufs::TransformationSequence* transformation_sequence_out, - Args&&... extra_args) const; + void MaybeAddRepeatedPass(RepeatedPassInstances* pass_instances, + Args&&... extra_args); // A convenience method to add a final fuzzer pass to |passes| with // probability 0.5, or with probability 1 if |enable_all_passes_| is true. // - // All fuzzer passes take |ir_context|, |transformation_context|, - // |fuzzer_context| and |transformation_sequence_out| as parameters. Extra + // All fuzzer passes take members |ir_context_|, |transformation_context_|, + // |fuzzer_context_| and |transformation_sequence_out_| as parameters. Extra // arguments can be provided via |extra_args|. template - void MaybeAddFinalPass( - std::vector>* passes, - opt::IRContext* ir_context, TransformationContext* transformation_context, - FuzzerContext* fuzzer_context, - protobufs::TransformationSequence* transformation_sequence_out, - Args&&... extra_args) const; + void MaybeAddFinalPass(std::vector>* passes, + Args&&... extra_args); // Decides whether to apply more repeated passes. The probability decreases as // the number of transformations that have been applied increases. - bool ShouldContinueFuzzing( - const protobufs::TransformationSequence& transformation_sequence_out, - FuzzerContext* fuzzer_context); + bool ShouldContinueFuzzing(); // Applies |pass|, which must be a pass constructed with |ir_context|, and // then returns true if and only if |ir_context| is valid. |tools| is used to // check validity. bool ApplyPassAndCheckValidity(FuzzerPass* pass, - const opt::IRContext& ir_context, const spvtools::SpirvTools& tools) const; // Target environment. const spv_target_env target_env_; - // Message consumer. + // Message consumer that will be invoked once for each message communicated + // from the library. MessageConsumer consumer_; - // Seed for random number generator. - const uint32_t seed_; + // The initial binary to which fuzzing should be applied. + const std::vector& binary_in_; + + // Initial facts known to hold in advance of applying any transformations. + const protobufs::FactSequence& initial_facts_; + + // A source of modules whose contents can be donated into the module being + // fuzzed. + const std::vector& donor_suppliers_; + + // Random number generator to control decision making during fuzzing. + std::unique_ptr random_generator_; // Determines whether all passes should be enabled, vs. having passes be // probabilistically enabled. @@ -155,6 +153,20 @@ class Fuzzer { // of, in order to enforce a hard limit on the number of times such passes // can be applied. uint32_t num_repeated_passes_applied_; + + // Intermediate representation for the module being fuzzed, which gets + // mutated as fuzzing proceeds. + std::unique_ptr ir_context_; + + // Provides probabilities that control the fuzzing process. + std::unique_ptr fuzzer_context_; + + // Contextual information that is required in order to apply transformations. + std::unique_ptr transformation_context_; + + // The sequence of transformations that have been applied during fuzzing. It + // is initially empty and grows as fuzzer passes are applied. + protobufs::TransformationSequence transformation_sequence_out_; }; } // namespace fuzz diff --git a/source/fuzz/replayer.cpp b/source/fuzz/replayer.cpp index d439b9dfe..df2b84878 100644 --- a/source/fuzz/replayer.cpp +++ b/source/fuzz/replayer.cpp @@ -28,81 +28,88 @@ namespace spvtools { namespace fuzz { -Replayer::Replayer(spv_target_env target_env, bool validate_during_replay, - spv_validator_options validator_options) +Replayer::Replayer( + spv_target_env target_env, MessageConsumer consumer, + const std::vector& binary_in, + const protobufs::FactSequence& initial_facts, + const protobufs::TransformationSequence& transformation_sequence_in, + uint32_t num_transformations_to_apply, uint32_t first_overflow_id, + bool validate_during_replay, spv_validator_options validator_options) : target_env_(target_env), + consumer_(std::move(consumer)), + binary_in_(binary_in), + initial_facts_(initial_facts), + transformation_sequence_in_(transformation_sequence_in), + num_transformations_to_apply_(num_transformations_to_apply), + first_overflow_id_(first_overflow_id), validate_during_replay_(validate_during_replay), validator_options_(validator_options) {} Replayer::~Replayer() = default; -void Replayer::SetMessageConsumer(MessageConsumer consumer) { - consumer_ = std::move(consumer); -} - -Replayer::ReplayerResultStatus Replayer::Run( - const std::vector& binary_in, - const protobufs::FactSequence& initial_facts, - const protobufs::TransformationSequence& transformation_sequence_in, - uint32_t num_transformations_to_apply, uint32_t first_overflow_id, - std::vector* binary_out, - protobufs::TransformationSequence* transformation_sequence_out) const { +Replayer::ReplayerResult Replayer::Run() { // Check compatibility between the library version being linked with and the // header files being used. GOOGLE_PROTOBUF_VERIFY_VERSION; - if (num_transformations_to_apply > - static_cast(transformation_sequence_in.transformation_size())) { + if (num_transformations_to_apply_ > + static_cast( + transformation_sequence_in_.transformation_size())) { consumer_(SPV_MSG_ERROR, nullptr, {}, "The number of transformations to be replayed must not " "exceed the size of the transformation sequence."); - return Replayer::ReplayerResultStatus::kTooManyTransformationsRequested; + return {Replayer::ReplayerResultStatus::kTooManyTransformationsRequested, + std::vector(), protobufs::TransformationSequence()}; } spvtools::SpirvTools tools(target_env_); if (!tools.IsValid()) { consumer_(SPV_MSG_ERROR, nullptr, {}, "Failed to create SPIRV-Tools interface; stopping."); - return Replayer::ReplayerResultStatus::kFailedToCreateSpirvToolsInterface; + return {Replayer::ReplayerResultStatus::kFailedToCreateSpirvToolsInterface, + std::vector(), protobufs::TransformationSequence()}; } // Initial binary should be valid. - if (!tools.Validate(&binary_in[0], binary_in.size(), validator_options_)) { + if (!tools.Validate(&binary_in_[0], binary_in_.size(), validator_options_)) { consumer_(SPV_MSG_INFO, nullptr, {}, "Initial binary is invalid; stopping."); - return Replayer::ReplayerResultStatus::kInitialBinaryInvalid; + return {Replayer::ReplayerResultStatus::kInitialBinaryInvalid, + std::vector(), protobufs::TransformationSequence()}; } // Build the module from the input binary. std::unique_ptr ir_context = - BuildModule(target_env_, consumer_, binary_in.data(), binary_in.size()); + BuildModule(target_env_, consumer_, binary_in_.data(), binary_in_.size()); assert(ir_context); // For replay validation, we track the last valid SPIR-V binary that was // observed. Initially this is the input binary. std::vector last_valid_binary; if (validate_during_replay_) { - last_valid_binary = binary_in; + last_valid_binary = binary_in_; } FactManager fact_manager; - fact_manager.AddFacts(consumer_, initial_facts, ir_context.get()); + fact_manager.AddFacts(consumer_, initial_facts_, ir_context.get()); std::unique_ptr transformation_context = - first_overflow_id == 0 + first_overflow_id_ == 0 ? MakeUnique(&fact_manager, validator_options_) : MakeUnique( &fact_manager, validator_options_, - MakeUnique(first_overflow_id)); + MakeUnique(first_overflow_id_)); // We track the largest id bound observed, to ensure that it only increases // as transformations are applied. uint32_t max_observed_id_bound = ir_context->module()->id_bound(); (void)(max_observed_id_bound); // Keep release-mode compilers happy. + protobufs::TransformationSequence transformation_sequence_out; + // Consider the transformation proto messages in turn. uint32_t counter = 0; - for (auto& message : transformation_sequence_in.transformation()) { - if (counter >= num_transformations_to_apply) { + for (auto& message : transformation_sequence_in_.transformation()) { + if (counter >= num_transformations_to_apply_) { break; } counter++; @@ -115,7 +122,7 @@ Replayer::ReplayerResultStatus Replayer::Run( // The transformation is applicable, so apply it, and copy it to the // sequence of transformations that were applied. transformation->Apply(ir_context.get(), transformation_context.get()); - *transformation_sequence_out->add_transformation() = message; + *transformation_sequence_out.add_transformation() = message; assert(ir_context->module()->id_bound() >= max_observed_id_bound && "The module's id bound should only increase due to applying " @@ -132,7 +139,8 @@ Replayer::ReplayerResultStatus Replayer::Run( consumer_(SPV_MSG_INFO, nullptr, {}, "Binary became invalid during replay (set a " "breakpoint to inspect); stopping."); - return Replayer::ReplayerResultStatus::kReplayValidationFailure; + return {Replayer::ReplayerResultStatus::kReplayValidationFailure, + std::vector(), protobufs::TransformationSequence()}; } // The binary was valid, so it becomes the latest valid binary. @@ -142,8 +150,10 @@ Replayer::ReplayerResultStatus Replayer::Run( } // Write out the module as a binary. - ir_context->module()->ToBinary(binary_out, false); - return Replayer::ReplayerResultStatus::kComplete; + std::vector binary_out; + ir_context->module()->ToBinary(&binary_out, false); + return {Replayer::ReplayerResultStatus::kComplete, std::move(binary_out), + std::move(transformation_sequence_out)}; } } // namespace fuzz diff --git a/source/fuzz/replayer.h b/source/fuzz/replayer.h index a10e536a7..5bc62d9d0 100644 --- a/source/fuzz/replayer.h +++ b/source/fuzz/replayer.h @@ -29,7 +29,7 @@ namespace fuzz { class Replayer { public: // Possible statuses that can result from running the replayer. - enum ReplayerResultStatus { + enum class ReplayerResultStatus { kComplete, kFailedToCreateSpirvToolsInterface, kInitialBinaryInvalid, @@ -37,8 +37,18 @@ class Replayer { kTooManyTransformationsRequested, }; - // Constructs a replayer from the given target environment. - Replayer(spv_target_env target_env, bool validate_during_replay, + struct ReplayerResult { + ReplayerResultStatus status; + std::vector transformed_binary; + protobufs::TransformationSequence applied_transformations; + }; + + Replayer(spv_target_env target_env, MessageConsumer consumer, + const std::vector& binary_in, + const protobufs::FactSequence& initial_facts, + const protobufs::TransformationSequence& transformation_sequence_in, + uint32_t num_transformations_to_apply, uint32_t first_overflow_id, + bool validate_during_replay, spv_validator_options validator_options); // Disables copy/move constructor/assignment operations. @@ -49,31 +59,21 @@ class Replayer { ~Replayer(); - // Sets the message consumer to the given |consumer|. The |consumer| will be - // invoked once for each message communicated from the library. - void SetMessageConsumer(MessageConsumer consumer); - - // Transforms |binary_in| to |binary_out| by attempting to apply the first - // |num_transformations_to_apply| transformations from - // |transformation_sequence_in|. + // Attempts to apply the first |num_transformations_to_apply_| transformations + // from |transformation_sequence_in_| to |binary_in_|. Initial facts about + // the input binary and the context in which it will execute are provided via + // |initial_facts_|. // - // Initial facts about the input binary and the context in which it will - // execute are provided via |initial_facts|. - // - // |first_overflow_id| should be set to 0 if overflow ids are not available - // during replay. Otherwise |first_overflow_id| must be larger than any id - // referred to in |binary_in| or |transformation_sequence_in|, and overflow + // |first_overflow_id_| should be set to 0 if overflow ids are not available + // during replay. Otherwise |first_overflow_id_| must be larger than any id + // referred to in |binary_in_| or |transformation_sequence_in_|, and overflow // ids will be available during replay starting from this value. // - // The transformations that were successfully applied are returned via - // |transformation_sequence_out|. - ReplayerResultStatus Run( - const std::vector& binary_in, - const protobufs::FactSequence& initial_facts, - const protobufs::TransformationSequence& transformation_sequence_in, - uint32_t num_transformations_to_apply, uint32_t first_overflow_id, - std::vector* binary_out, - protobufs::TransformationSequence* transformation_sequence_out) const; + // On success, returns a successful result status together with the + // transformations that were successfully applied and the binary resulting + // from applying them. Otherwise, returns an appropriate result status + // together with an empty binary and empty transformation sequence. + ReplayerResult Run(); private: // Target environment. @@ -82,6 +82,22 @@ class Replayer { // Message consumer. MessageConsumer consumer_; + // The binary to which transformations are to be applied. + const std::vector& binary_in_; + + // Initial facts known to hold in advance of applying any transformations. + const protobufs::FactSequence& initial_facts_; + + // The transformations to be replayed. + const protobufs::TransformationSequence& transformation_sequence_in_; + + // The number of transformations that should be replayed. + const uint32_t num_transformations_to_apply_; + + // Zero if overflow ids are not available, otherwise hold the value of the + // smallest id that may be used for overflow purposes. + const uint32_t first_overflow_id_; + // Controls whether the validator should be run after every replay step. const bool validate_during_replay_; diff --git a/source/fuzz/shrinker.cpp b/source/fuzz/shrinker.cpp index 7b88405a3..a88a1eaf2 100644 --- a/source/fuzz/shrinker.cpp +++ b/source/fuzz/shrinker.cpp @@ -61,35 +61,27 @@ protobufs::TransformationSequence RemoveChunk( } // namespace -uint32_t Shrinker::GetIdBound(const std::vector& binary) const { - // Build the module from the input binary. - std::unique_ptr ir_context = - BuildModule(target_env_, consumer_, binary.data(), binary.size()); - assert(ir_context && "Error building module."); - return ir_context->module()->id_bound(); -} - -Shrinker::Shrinker(spv_target_env target_env, uint32_t step_limit, - bool validate_during_replay, - spv_validator_options validator_options) +Shrinker::Shrinker( + spv_target_env target_env, MessageConsumer consumer, + const std::vector& binary_in, + const protobufs::FactSequence& initial_facts, + const protobufs::TransformationSequence& transformation_sequence_in, + const InterestingnessFunction& interestingness_function, + uint32_t step_limit, bool validate_during_replay, + spv_validator_options validator_options) : target_env_(target_env), + consumer_(consumer), + binary_in_(binary_in), + initial_facts_(initial_facts), + transformation_sequence_in_(transformation_sequence_in), + interestingness_function_(interestingness_function), step_limit_(step_limit), validate_during_replay_(validate_during_replay), validator_options_(validator_options) {} Shrinker::~Shrinker() = default; -void Shrinker::SetMessageConsumer(MessageConsumer consumer) { - consumer_ = std::move(consumer); -} - -Shrinker::ShrinkerResultStatus Shrinker::Run( - const std::vector& binary_in, - const protobufs::FactSequence& initial_facts, - const protobufs::TransformationSequence& transformation_sequence_in, - const Shrinker::InterestingnessFunction& interestingness_function, - std::vector* binary_out, - protobufs::TransformationSequence* transformation_sequence_out) const { +Shrinker::ShrinkerResult Shrinker::Run() { // Check compatibility between the library version being linked with and the // header files being used. GOOGLE_PROTOBUF_VERIFY_VERSION; @@ -98,46 +90,54 @@ Shrinker::ShrinkerResultStatus Shrinker::Run( if (!tools.IsValid()) { consumer_(SPV_MSG_ERROR, nullptr, {}, "Failed to create SPIRV-Tools interface; stopping."); - return Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface; + return {Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface, + std::vector(), protobufs::TransformationSequence()}; } // Initial binary should be valid. - if (!tools.Validate(&binary_in[0], binary_in.size(), validator_options_)) { + if (!tools.Validate(&binary_in_[0], binary_in_.size(), validator_options_)) { consumer_(SPV_MSG_INFO, nullptr, {}, "Initial binary is invalid; stopping."); - return Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid; + return {Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid, + std::vector(), protobufs::TransformationSequence()}; } - std::vector current_best_binary; - protobufs::TransformationSequence current_best_transformations; - - // Run a replay of the initial transformation sequence to (a) check that it - // succeeds, (b) get the binary that results from running these - // transformations, and (c) get the subsequence of the initial transformations - // that actually apply (in principle this could be a strict subsequence). - Replayer replayer(target_env_, validate_during_replay_, validator_options_); - replayer.SetMessageConsumer(consumer_); - if (replayer.Run(binary_in, initial_facts, transformation_sequence_in, - static_cast( - transformation_sequence_in.transformation_size()), - /* No overflow ids */ 0, ¤t_best_binary, - ¤t_best_transformations) != + // Run a replay of the initial transformation sequence to check that it + // succeeds. + auto initial_replay_result = + Replayer(target_env_, consumer_, binary_in_, initial_facts_, + transformation_sequence_in_, + static_cast( + transformation_sequence_in_.transformation_size()), + /* No overflow ids */ 0, validate_during_replay_, + validator_options_) + .Run(); + if (initial_replay_result.status != Replayer::ReplayerResultStatus::kComplete) { - return ShrinkerResultStatus::kReplayFailed; + return {ShrinkerResultStatus::kReplayFailed, std::vector(), + protobufs::TransformationSequence()}; } + // Get the binary that results from running these transformations, and the + // subsequence of the initial transformations that actually apply (in + // principle this could be a strict subsequence). + std::vector current_best_binary = + std::move(initial_replay_result.transformed_binary); + protobufs::TransformationSequence current_best_transformations = + std::move(initial_replay_result.applied_transformations); // Check that the binary produced by applying the initial transformations is // indeed interesting. - if (!interestingness_function(current_best_binary, 0)) { + if (!interestingness_function_(current_best_binary, 0)) { consumer_(SPV_MSG_INFO, nullptr, {}, "Initial binary is not interesting; stopping."); - return ShrinkerResultStatus::kInitialBinaryNotInteresting; + return {ShrinkerResultStatus::kInitialBinaryNotInteresting, + std::vector(), protobufs::TransformationSequence()}; } // The largest id used by the module before any shrinking has been applied // serves as the first id that can be used for overflow purposes. const uint32_t first_overflow_id = GetIdBound(current_best_binary); - assert(first_overflow_id >= GetIdBound(binary_in) && + assert(first_overflow_id >= GetIdBound(binary_in_) && "Applying transformations should only increase a module's id bound."); uint32_t attempt = 0; // Keeps track of the number of shrink attempts that @@ -195,29 +195,34 @@ Shrinker::ShrinkerResultStatus Shrinker::Run( // replay might be even smaller than the transformations with the chunk // removed, because removing those transformations might make further // transformations inapplicable. - std::vector next_binary; - protobufs::TransformationSequence next_transformation_sequence; - if (replayer.Run( - binary_in, initial_facts, transformations_with_chunk_removed, + auto replay_result = + Replayer( + target_env_, consumer_, binary_in_, initial_facts_, + transformations_with_chunk_removed, static_cast( transformations_with_chunk_removed.transformation_size()), - first_overflow_id, &next_binary, &next_transformation_sequence) != - Replayer::ReplayerResultStatus::kComplete) { + first_overflow_id, validate_during_replay_, validator_options_) + .Run(); + if (replay_result.status != Replayer::ReplayerResultStatus::kComplete) { // Replay should not fail; if it does, we need to abort shrinking. - return ShrinkerResultStatus::kReplayFailed; + return {ShrinkerResultStatus::kReplayFailed, std::vector(), + protobufs::TransformationSequence()}; } - assert(NumRemainingTransformations(next_transformation_sequence) >= - chunk_index * chunk_size && - "Removing this chunk of transformations should not have an effect " - "on earlier chunks."); + assert( + NumRemainingTransformations(replay_result.applied_transformations) >= + chunk_index * chunk_size && + "Removing this chunk of transformations should not have an effect " + "on earlier chunks."); - if (interestingness_function(next_binary, attempt)) { + if (interestingness_function_(replay_result.transformed_binary, + attempt)) { // If the binary arising from the smaller transformation sequence is // interesting, this becomes our current best binary and transformation // sequence. - current_best_binary = next_binary; - current_best_transformations = next_transformation_sequence; + current_best_binary = std::move(replay_result.transformed_binary); + current_best_transformations = + std::move(replay_result.applied_transformations); progress_this_round = true; } // Either way, this was a shrink attempt, so increment our count of shrink @@ -237,22 +242,32 @@ Shrinker::ShrinkerResultStatus Shrinker::Run( } } - // The output from the shrinker is the best binary we saw, and the - // transformations that led to it. - *binary_out = current_best_binary; - *transformation_sequence_out = current_best_transformations; - // Indicate whether shrinking completed or was truncated due to reaching the // step limit. + // + // Either way, the output from the shrinker is the best binary we saw, and the + // transformations that led to it. assert(attempt <= step_limit_); if (attempt == step_limit_) { std::stringstream strstream; strstream << "Shrinking did not complete; step limit " << step_limit_ << " was reached."; consumer_(SPV_MSG_WARNING, nullptr, {}, strstream.str().c_str()); - return Shrinker::ShrinkerResultStatus::kStepLimitReached; + return {Shrinker::ShrinkerResultStatus::kStepLimitReached, + std::move(current_best_binary), + std::move(current_best_transformations)}; } - return Shrinker::ShrinkerResultStatus::kComplete; + return {Shrinker::ShrinkerResultStatus::kComplete, + std::move(current_best_binary), + std::move(current_best_transformations)}; +} + +uint32_t Shrinker::GetIdBound(const std::vector& binary) const { + // Build the module from the input binary. + std::unique_ptr ir_context = + BuildModule(target_env_, consumer_, binary.data(), binary.size()); + assert(ir_context && "Error building module."); + return ir_context->module()->id_bound(); } } // namespace fuzz diff --git a/source/fuzz/shrinker.h b/source/fuzz/shrinker.h index 0fe89290d..982a84310 100644 --- a/source/fuzz/shrinker.h +++ b/source/fuzz/shrinker.h @@ -30,7 +30,7 @@ namespace fuzz { class Shrinker { public: // Possible statuses that can result from running the shrinker. - enum ShrinkerResultStatus { + enum class ShrinkerResultStatus { kComplete, kFailedToCreateSpirvToolsInterface, kInitialBinaryInvalid, @@ -39,6 +39,12 @@ class Shrinker { kStepLimitReached, }; + struct ShrinkerResult { + ShrinkerResultStatus status; + std::vector transformed_binary; + protobufs::TransformationSequence applied_transformations; + }; + // The type for a function that will take a binary, |binary|, and return true // if and only if the binary is deemed interesting. (The function also takes // an integer argument, |counter|, that will be incremented each time the @@ -49,8 +55,12 @@ class Shrinker { using InterestingnessFunction = std::function& binary, uint32_t counter)>; - Shrinker(spv_target_env target_env, uint32_t step_limit, - bool validate_during_replay, + Shrinker(spv_target_env target_env, MessageConsumer consumer, + const std::vector& binary_in, + const protobufs::FactSequence& initial_facts, + const protobufs::TransformationSequence& transformation_sequence_in, + const InterestingnessFunction& interestingness_function, + uint32_t step_limit, bool validate_during_replay, spv_validator_options validator_options); // Disables copy/move constructor/assignment operations. @@ -61,25 +71,20 @@ class Shrinker { ~Shrinker(); - // Sets the message consumer to the given |consumer|. The |consumer| will be - // invoked once for each message communicated from the library. - void SetMessageConsumer(MessageConsumer consumer); - - // Requires that when |transformation_sequence_in| is applied to |binary_in| - // with initial facts |initial_facts|, the resulting binary is interesting - // according to |interestingness_function|. + // Requires that when |transformation_sequence_in_| is applied to |binary_in_| + // with initial facts |initial_facts_|, the resulting binary is interesting + // according to |interestingness_function_|. // - // Produces, via |transformation_sequence_out|, a subsequence of - // |transformation_sequence_in| that, when applied with initial facts - // |initial_facts|, produces a binary (captured via |binary_out|) that is - // also interesting according to |interestingness_function|. - ShrinkerResultStatus Run( - const std::vector& binary_in, - const protobufs::FactSequence& initial_facts, - const protobufs::TransformationSequence& transformation_sequence_in, - const InterestingnessFunction& interestingness_function, - std::vector* binary_out, - protobufs::TransformationSequence* transformation_sequence_out) const; + // If shrinking succeeded -- possibly terminating early due to reaching the + // shrinker's step limit -- an associated result status is returned together + // with a subsequence of |transformation_sequence_in_| that, when applied + // to |binary_in_| with initial facts |initial_facts_|, produces a binary + // that is also interesting according to |interestingness_function_|; this + // binary is also returned. + // + // If shrinking failed for some reason, an appropriate result status is + // returned together with an empty binary and empty transformation sequence. + ShrinkerResult Run(); private: // Returns the id bound for the given SPIR-V binary, which is assumed to be @@ -89,9 +94,22 @@ class Shrinker { // Target environment. const spv_target_env target_env_; - // Message consumer. + // Message consumer that will be invoked once for each message communicated + // from the library. MessageConsumer consumer_; + // The binary to which transformations are to be applied. + const std::vector& binary_in_; + + // Initial facts known to hold in advance of applying any transformations. + const protobufs::FactSequence& initial_facts_; + + // The series of transformations to be shrunk. + const protobufs::TransformationSequence& transformation_sequence_in_; + + // Function that decides whether a given binary is interesting. + const InterestingnessFunction& interestingness_function_; + // Step limit to decide when to terminate shrinking early. const uint32_t step_limit_; diff --git a/test/fuzz/fuzzer_replayer_test.cpp b/test/fuzz/fuzzer_replayer_test.cpp index eb2b81790..bfcf4ea9d 100644 --- a/test/fuzz/fuzzer_replayer_test.cpp +++ b/test/fuzz/fuzzer_replayer_test.cpp @@ -14,6 +14,7 @@ #include "source/fuzz/fuzzer.h" #include "source/fuzz/fuzzer_util.h" +#include "source/fuzz/pseudo_random_generator.h" #include "source/fuzz/replayer.h" #include "source/fuzz/uniform_buffer_element_descriptor.h" #include "test/fuzz/fuzz_test_util.h" @@ -1645,51 +1646,46 @@ void RunFuzzerAndReplayer(const std::string& shader, Fuzzer::RepeatedPassStrategy::kRandomWithRecommendations}; uint32_t strategy_index = 0; for (uint32_t seed = initial_seed; seed < initial_seed + num_runs; seed++) { - std::vector fuzzer_binary_out; - protobufs::TransformationSequence fuzzer_transformation_sequence_out; - spvtools::ValidatorOptions validator_options; // Every 4th time we run the fuzzer, enable all fuzzer passes. bool enable_all_passes = (seed % 4) == 0; - Fuzzer fuzzer(env, seed, enable_all_passes, strategies[strategy_index], - true, validator_options); + auto fuzzer_result = + Fuzzer(env, kSilentConsumer, binary_in, initial_facts, donor_suppliers, + MakeUnique(seed), enable_all_passes, + strategies[strategy_index], true, validator_options) + .Run(); // Cycle the repeated pass strategy so that we try a different one next time // we run the fuzzer. strategy_index = (strategy_index + 1) % static_cast(strategies.size()); - fuzzer.SetMessageConsumer(kConsoleMessageConsumer); - auto fuzzer_result_status = - fuzzer.Run(binary_in, initial_facts, donor_suppliers, - &fuzzer_binary_out, &fuzzer_transformation_sequence_out); - ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result_status); - ASSERT_TRUE(t.Validate(fuzzer_binary_out)); + ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result.status); + ASSERT_TRUE(t.Validate(fuzzer_result.transformed_binary)); - std::vector replayer_binary_out; - protobufs::TransformationSequence replayer_transformation_sequence_out; - - Replayer replayer(env, false, validator_options); - replayer.SetMessageConsumer(kConsoleMessageConsumer); - auto replayer_result_status = replayer.Run( - binary_in, initial_facts, fuzzer_transformation_sequence_out, - static_cast( - fuzzer_transformation_sequence_out.transformation_size()), - 0, &replayer_binary_out, &replayer_transformation_sequence_out); + auto replayer_result = + Replayer( + env, kConsoleMessageConsumer, binary_in, initial_facts, + fuzzer_result.applied_transformations, + static_cast( + fuzzer_result.applied_transformations.transformation_size()), + 0, false, validator_options) + .Run(); ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete, - replayer_result_status); + replayer_result.status); // After replaying the transformations applied by the fuzzer, exactly those // transformations should have been applied, and the binary resulting from // replay should be identical to that which resulted from fuzzing. std::string fuzzer_transformations_string; std::string replayer_transformations_string; - fuzzer_transformation_sequence_out.SerializeToString( + fuzzer_result.applied_transformations.SerializeToString( &fuzzer_transformations_string); - replayer_transformation_sequence_out.SerializeToString( + replayer_result.applied_transformations.SerializeToString( &replayer_transformations_string); ASSERT_EQ(fuzzer_transformations_string, replayer_transformations_string); - ASSERT_EQ(fuzzer_binary_out, replayer_binary_out); + ASSERT_EQ(fuzzer_result.transformed_binary, + replayer_result.transformed_binary); } } diff --git a/test/fuzz/fuzzer_shrinker_test.cpp b/test/fuzz/fuzzer_shrinker_test.cpp index c9ae42aaf..361f5e812 100644 --- a/test/fuzz/fuzzer_shrinker_test.cpp +++ b/test/fuzz/fuzzer_shrinker_test.cpp @@ -991,25 +991,25 @@ void RunAndCheckShrinker( uint32_t expected_transformations_out_size, uint32_t step_limit, spv_validator_options validator_options) { // Run the shrinker. - Shrinker shrinker(target_env, step_limit, false, validator_options); - shrinker.SetMessageConsumer(kSilentConsumer); + auto shrinker_result = + Shrinker(target_env, kSilentConsumer, binary_in, initial_facts, + transformation_sequence_in, interestingness_function, step_limit, + false, validator_options) + .Run(); - std::vector binary_out; - protobufs::TransformationSequence transformations_out; - Shrinker::ShrinkerResultStatus shrinker_result_status = - shrinker.Run(binary_in, initial_facts, transformation_sequence_in, - interestingness_function, &binary_out, &transformations_out); ASSERT_TRUE(Shrinker::ShrinkerResultStatus::kComplete == - shrinker_result_status || + shrinker_result.status || Shrinker::ShrinkerResultStatus::kStepLimitReached == - shrinker_result_status); + shrinker_result.status); // If a non-empty expected binary was provided, check that it matches the // result of shrinking and that the expected number of transformations remain. if (!expected_binary_out.empty()) { - ASSERT_EQ(expected_binary_out, binary_out); - ASSERT_EQ(expected_transformations_out_size, - static_cast(transformations_out.transformation_size())); + ASSERT_EQ(expected_binary_out, shrinker_result.transformed_binary); + ASSERT_EQ( + expected_transformations_out_size, + static_cast( + shrinker_result.applied_transformations.transformation_size())); } } @@ -1037,8 +1037,6 @@ void RunFuzzerAndShrinker(const std::string& shader, } // Run the fuzzer and check that it successfully yields a valid binary. - std::vector fuzzer_binary_out; - protobufs::TransformationSequence fuzzer_transformation_sequence_out; spvtools::ValidatorOptions validator_options; // Depending on the seed, decide whether to enable all passes and which @@ -1055,14 +1053,13 @@ void RunFuzzerAndShrinker(const std::string& shader, Fuzzer::RepeatedPassStrategy::kRandomWithRecommendations; } - Fuzzer fuzzer(env, seed, enable_all_passes, repeated_pass_strategy, true, - validator_options); - fuzzer.SetMessageConsumer(kSilentConsumer); - auto fuzzer_result_status = - fuzzer.Run(binary_in, initial_facts, donor_suppliers, &fuzzer_binary_out, - &fuzzer_transformation_sequence_out); - ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result_status); - ASSERT_TRUE(t.Validate(fuzzer_binary_out)); + auto fuzzer_result = + Fuzzer(env, kSilentConsumer, binary_in, initial_facts, donor_suppliers, + MakeUnique(seed), enable_all_passes, + repeated_pass_strategy, true, validator_options) + .Run(); + ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result.status); + ASSERT_TRUE(t.Validate(fuzzer_result.transformed_binary)); const uint32_t kReasonableStepLimit = 50; const uint32_t kSmallStepLimit = 20; @@ -1070,30 +1067,30 @@ void RunFuzzerAndShrinker(const std::string& shader, // With the AlwaysInteresting test, we should quickly shrink to the original // binary with no transformations remaining. RunAndCheckShrinker(env, binary_in, initial_facts, - fuzzer_transformation_sequence_out, + fuzzer_result.applied_transformations, AlwaysInteresting().AsFunction(), binary_in, 0, kReasonableStepLimit, validator_options); // With the OnlyInterestingFirstTime test, no shrinking should be achieved. RunAndCheckShrinker( - env, binary_in, initial_facts, fuzzer_transformation_sequence_out, - OnlyInterestingFirstTime().AsFunction(), fuzzer_binary_out, + env, binary_in, initial_facts, fuzzer_result.applied_transformations, + OnlyInterestingFirstTime().AsFunction(), fuzzer_result.transformed_binary, static_cast( - fuzzer_transformation_sequence_out.transformation_size()), + fuzzer_result.applied_transformations.transformation_size()), kReasonableStepLimit, validator_options); // The PingPong test is unpredictable; passing an empty expected binary // means that we don't check anything beyond that shrinking completes // successfully. RunAndCheckShrinker( - env, binary_in, initial_facts, fuzzer_transformation_sequence_out, + env, binary_in, initial_facts, fuzzer_result.applied_transformations, PingPong().AsFunction(), {}, 0, kSmallStepLimit, validator_options); // The InterestingThenRandom test is unpredictable; passing an empty // expected binary means that we do not check anything about shrinking // results. RunAndCheckShrinker( - env, binary_in, initial_facts, fuzzer_transformation_sequence_out, + env, binary_in, initial_facts, fuzzer_result.applied_transformations, InterestingThenRandom(PseudoRandomGenerator(seed)).AsFunction(), {}, 0, kSmallStepLimit, validator_options); } diff --git a/test/fuzz/replayer_test.cpp b/test/fuzz/replayer_test.cpp index 39787d2af..2444e9f8d 100644 --- a/test/fuzz/replayer_test.cpp +++ b/test/fuzz/replayer_test.cpp @@ -89,20 +89,17 @@ TEST(ReplayerTest, PartialReplay) { { // Full replay - protobufs::TransformationSequence transformations_out; protobufs::FactSequence empty_facts; - std::vector binary_out; - Replayer replayer(env, true, validator_options); - replayer.SetMessageConsumer(kSilentConsumer); - auto replayer_result_status = - replayer.Run(binary_in, empty_facts, transformations, 11, 0, - &binary_out, &transformations_out); + auto replayer_result = + Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations, + 11, 0, true, validator_options) + .Run(); // Replay should succeed. ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete, - replayer_result_status); + replayer_result.status); // All transformations should be applied. ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals( - transformations, transformations_out)); + transformations, replayer_result.applied_transformations)); const std::string kFullySplitShader = R"( OpCapability Shader @@ -172,28 +169,26 @@ TEST(ReplayerTest, PartialReplay) { OpReturn OpFunctionEnd )"; - ASSERT_TRUE(IsEqual(env, kFullySplitShader, binary_out)); + ASSERT_TRUE( + IsEqual(env, kFullySplitShader, replayer_result.transformed_binary)); } { // Half replay - protobufs::TransformationSequence transformations_out; protobufs::FactSequence empty_facts; - std::vector binary_out; - Replayer replayer(env, true, validator_options); - replayer.SetMessageConsumer(kSilentConsumer); - auto replayer_result_status = - replayer.Run(binary_in, empty_facts, transformations, 5, 0, &binary_out, - &transformations_out); + auto replayer_result = + Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations, + 5, 0, true, validator_options) + .Run(); // Replay should succeed. ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete, - replayer_result_status); + replayer_result.status); // The first 5 transformations should be applied - ASSERT_EQ(5, transformations_out.transformation_size()); + ASSERT_EQ(5, replayer_result.applied_transformations.transformation_size()); for (uint32_t i = 0; i < 5; i++) { ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals( transformations.transformation(i), - transformations_out.transformation(i))); + replayer_result.applied_transformations.transformation(i))); } const std::string kHalfSplitShader = R"( @@ -252,47 +247,42 @@ TEST(ReplayerTest, PartialReplay) { OpReturn OpFunctionEnd )"; - ASSERT_TRUE(IsEqual(env, kHalfSplitShader, binary_out)); + ASSERT_TRUE( + IsEqual(env, kHalfSplitShader, replayer_result.transformed_binary)); } { // Empty replay - protobufs::TransformationSequence transformations_out; protobufs::FactSequence empty_facts; - std::vector binary_out; - Replayer replayer(env, true, validator_options); - replayer.SetMessageConsumer(kSilentConsumer); - auto replayer_result_status = - replayer.Run(binary_in, empty_facts, transformations, 0, 0, &binary_out, - &transformations_out); + auto replayer_result = + Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations, + 0, 0, true, validator_options) + .Run(); // Replay should succeed. ASSERT_EQ(Replayer::ReplayerResultStatus::kComplete, - replayer_result_status); + replayer_result.status); // No transformations should be applied - ASSERT_EQ(0, transformations_out.transformation_size()); - ASSERT_TRUE(IsEqual(env, kTestShader, binary_out)); + ASSERT_EQ(0, replayer_result.applied_transformations.transformation_size()); + ASSERT_TRUE(IsEqual(env, kTestShader, replayer_result.transformed_binary)); } { // Invalid replay: too many transformations - protobufs::TransformationSequence transformations_out; protobufs::FactSequence empty_facts; - std::vector binary_out; // The number of transformations requested to be applied exceeds the number // of transformations - Replayer replayer(env, true, validator_options); - replayer.SetMessageConsumer(kSilentConsumer); - auto replayer_result_status = - replayer.Run(binary_in, empty_facts, transformations, 12, 0, - &binary_out, &transformations_out); + auto replayer_result = + Replayer(env, kSilentConsumer, binary_in, empty_facts, transformations, + 12, 0, true, validator_options) + .Run(); // Replay should not succeed. ASSERT_EQ(Replayer::ReplayerResultStatus::kTooManyTransformationsRequested, - replayer_result_status); + replayer_result.status); // No transformations should be applied - ASSERT_EQ(0, transformations_out.transformation_size()); + ASSERT_EQ(0, replayer_result.applied_transformations.transformation_size()); // The output binary should be empty - ASSERT_TRUE(binary_out.empty()); + ASSERT_TRUE(replayer_result.transformed_binary.empty()); } } diff --git a/tools/fuzz/fuzz.cpp b/tools/fuzz/fuzz.cpp index ea7e0b709..80ac9f514 100644 --- a/tools/fuzz/fuzz.cpp +++ b/tools/fuzz/fuzz.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -24,12 +25,14 @@ #include "source/fuzz/fuzzer.h" #include "source/fuzz/fuzzer_util.h" #include "source/fuzz/protobufs/spirvfuzz_protobufs.h" +#include "source/fuzz/pseudo_random_generator.h" #include "source/fuzz/replayer.h" #include "source/fuzz/shrinker.h" #include "source/opt/build_module.h" #include "source/opt/ir_context.h" #include "source/opt/log.h" #include "source/spirv_fuzzer_options.h" +#include "source/util/make_unique.h" #include "source/util/string_utils.h" #include "tools/io.h" #include "tools/util/cli_consumer.h" @@ -453,9 +456,6 @@ bool Replay(const spv_target_env& target_env, &transformation_sequence)) { return false; } - spvtools::fuzz::Replayer replayer( - target_env, fuzzer_options->replay_validation_enabled, validator_options); - replayer.SetMessageConsumer(spvtools::utils::CLIMessageConsumer); uint32_t num_transformations_to_apply; if (fuzzer_options->replay_range > 0) { @@ -474,11 +474,17 @@ bool Replay(const spv_target_env& target_env, fuzzer_options->replay_range)); } - auto replay_result_status = replayer.Run( - binary_in, initial_facts, transformation_sequence, - num_transformations_to_apply, 0, binary_out, transformations_applied); - return !(replay_result_status != - spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete); + auto replay_result = + spvtools::fuzz::Replayer( + target_env, spvtools::utils::CLIMessageConsumer, binary_in, + initial_facts, transformation_sequence, num_transformations_to_apply, + 0, fuzzer_options->replay_validation_enabled, validator_options) + .Run(); + + *binary_out = std::move(replay_result.transformed_binary); + *transformations_applied = std::move(replay_result.applied_transformations); + return replay_result.status == + spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete; } bool Shrink(const spv_target_env& target_env, @@ -497,11 +503,6 @@ bool Shrink(const spv_target_env& target_env, &transformation_sequence)) { return false; } - spvtools::fuzz::Shrinker shrinker( - target_env, fuzzer_options->shrinker_step_limit, - fuzzer_options->replay_validation_enabled, validator_options); - shrinker.SetMessageConsumer(spvtools::utils::CLIMessageConsumer); - assert(!interestingness_command.empty() && "An error should have been raised because the interestingness_command " "is empty."); @@ -527,13 +528,20 @@ bool Shrink(const spv_target_env& target_env, return ExecuteCommand(command); }; - auto shrink_result_status = shrinker.Run( - binary_in, initial_facts, transformation_sequence, - interestingness_function, binary_out, transformations_applied); + auto shrink_result = + spvtools::fuzz::Shrinker( + target_env, spvtools::utils::CLIMessageConsumer, binary_in, + initial_facts, transformation_sequence, interestingness_function, + fuzzer_options->shrinker_step_limit, + fuzzer_options->replay_validation_enabled, validator_options) + .Run(); + + *binary_out = std::move(shrink_result.transformed_binary); + *transformations_applied = std::move(shrink_result.applied_transformations); return spvtools::fuzz::Shrinker::ShrinkerResultStatus::kComplete == - shrink_result_status || + shrink_result.status || spvtools::fuzz::Shrinker::ShrinkerResultStatus::kStepLimitReached == - shrink_result_status; + shrink_result.status; } bool Fuzz(const spv_target_env& target_env, @@ -571,18 +579,20 @@ bool Fuzz(const spv_target_env& target_env, }); } - spvtools::fuzz::Fuzzer fuzzer( - target_env, - fuzzer_options->has_random_seed - ? fuzzer_options->random_seed - : static_cast(std::random_device()()), - fuzzer_options->all_passes_enabled, repeated_pass_strategy, - fuzzer_options->fuzzer_pass_validation_enabled, validator_options); - fuzzer.SetMessageConsumer(message_consumer); - auto fuzz_result_status = - fuzzer.Run(binary_in, initial_facts, donor_suppliers, binary_out, - transformations_applied); - if (fuzz_result_status != + auto fuzz_result = + spvtools::fuzz::Fuzzer( + target_env, message_consumer, binary_in, initial_facts, + donor_suppliers, + spvtools::MakeUnique( + fuzzer_options->has_random_seed + ? fuzzer_options->random_seed + : static_cast(std::random_device()())), + fuzzer_options->all_passes_enabled, repeated_pass_strategy, + fuzzer_options->fuzzer_pass_validation_enabled, validator_options) + .Run(); + *binary_out = std::move(fuzz_result.transformed_binary); + *transformations_applied = std::move(fuzz_result.applied_transformations); + if (fuzz_result.status != spvtools::fuzz::Fuzzer::FuzzerResultStatus::kComplete) { spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error running fuzzer"); return false;