// Copyright (c) 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "source/fuzz/added_function_reducer.h" #include "source/fuzz/instruction_message.h" #include "source/fuzz/replayer.h" #include "source/fuzz/transformation_add_function.h" #include "source/opt/build_module.h" #include "source/opt/ir_context.h" #include "source/reduce/reducer.h" namespace spvtools { namespace fuzz { AddedFunctionReducer::AddedFunctionReducer( 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 index_of_add_function_transformation, const Shrinker::InterestingnessFunction& shrinker_interestingness_function, bool validate_during_replay, spv_validator_options validator_options, uint32_t shrinker_step_limit, uint32_t num_existing_shrink_attempts) : target_env_(target_env), consumer_(std::move(consumer)), binary_in_(binary_in), initial_facts_(initial_facts), transformation_sequence_in_(transformation_sequence_in), index_of_add_function_transformation_( index_of_add_function_transformation), shrinker_interestingness_function_(shrinker_interestingness_function), validate_during_replay_(validate_during_replay), validator_options_(validator_options), shrinker_step_limit_(shrinker_step_limit), num_existing_shrink_attempts_(num_existing_shrink_attempts), num_reducer_interestingness_function_invocations_(0) {} AddedFunctionReducer::~AddedFunctionReducer() = default; AddedFunctionReducer::AddedFunctionReducerResult AddedFunctionReducer::Run() { // Replay all transformations before the AddFunction transformation, then // add the raw function associated with the AddFunction transformation. std::vector binary_to_reduce; std::unordered_set irrelevant_pointee_global_variables; ReplayPrefixAndAddFunction(&binary_to_reduce, &irrelevant_pointee_global_variables); // Set up spirv-reduce to use our very specific interestingness function. reduce::Reducer reducer(target_env_); reducer.SetMessageConsumer(consumer_); reducer.AddDefaultReductionPasses(); reducer.SetInterestingnessFunction( [this, &irrelevant_pointee_global_variables]( const std::vector& binary_under_reduction, uint32_t /*unused*/) { return InterestingnessFunctionForReducingAddedFunction( binary_under_reduction, irrelevant_pointee_global_variables); }); // Instruct spirv-reduce to only target the function with the id associated // with the AddFunction transformation that we care about. spvtools::ReducerOptions reducer_options; reducer_options.set_target_function(GetAddedFunctionId()); // Bound the number of reduction steps that spirv-reduce can make according // to the overall shrinker step limit and the number of shrink attempts that // have already been tried. assert(shrinker_step_limit_ > num_existing_shrink_attempts_ && "The added function reducer should not have been invoked."); reducer_options.set_step_limit(shrinker_step_limit_ - num_existing_shrink_attempts_); // Run spirv-reduce. std::vector reduced_binary; auto reducer_result = reducer.Run(std::move(binary_to_reduce), &reduced_binary, reducer_options, validator_options_); if (reducer_result != reduce::Reducer::kComplete && reducer_result != reduce::Reducer::kReachedStepLimit) { return {AddedFunctionReducerResultStatus::kReductionFailed, std::vector(), protobufs::TransformationSequence(), 0}; } // Provide the outer shrinker with an adapted sequence of transformations in // which the AddFunction transformation of interest has been simplified to use // the version of the added function that appears in |reduced_binary|. std::vector binary_out; protobufs::TransformationSequence transformation_sequence_out; ReplayAdaptedTransformations(reduced_binary, &binary_out, &transformation_sequence_out); // We subtract 1 from |num_reducer_interestingness_function_invocations_| to // account for the fact that spirv-reduce invokes its interestingness test // once before reduction commences in order to check that the initial module // is interesting. assert(num_reducer_interestingness_function_invocations_ > 0 && "At a minimum spirv-reduce should have invoked its interestingness " "test once."); return {AddedFunctionReducerResultStatus::kComplete, std::move(binary_out), std::move(transformation_sequence_out), num_reducer_interestingness_function_invocations_ - 1}; } bool AddedFunctionReducer::InterestingnessFunctionForReducingAddedFunction( const std::vector& binary_under_reduction, const std::unordered_set& irrelevant_pointee_global_variables) { uint32_t counter_for_shrinker_interestingness_function = num_existing_shrink_attempts_ + num_reducer_interestingness_function_invocations_; num_reducer_interestingness_function_invocations_++; // The reduced version of the added function must be limited to accessing // global variables appearing in |irrelevant_pointee_global_variables|. This // is to guard against the possibility of spirv-reduce changing a reference // to an irrelevant global to a reference to a regular global variable, which // could cause the added function to change the semantics of the original // module. auto ir_context = BuildModule(target_env_, consumer_, binary_under_reduction.data(), binary_under_reduction.size()); assert(ir_context != nullptr && "The binary should be parsable."); for (auto& type_or_value : ir_context->module()->types_values()) { if (type_or_value.opcode() != spv::Op::OpVariable) { continue; } if (irrelevant_pointee_global_variables.count(type_or_value.result_id())) { continue; } if (!ir_context->get_def_use_mgr()->WhileEachUse( &type_or_value, [this, &ir_context](opt::Instruction* user, uint32_t /*unused*/) -> bool { auto block = ir_context->get_instr_block(user); if (block != nullptr && block->GetParent()->result_id() == GetAddedFunctionId()) { return false; } return true; })) { return false; } } // For the binary to be deemed interesting, it must be possible to // successfully apply all the transformations, with the transformation at // index |index_of_add_function_transformation_| simplified to use the version // of the added function from |binary_under_reduction|. // // This might not be the case: spirv-reduce might have removed a chunk of the // added function on which future transformations depend. // // This is an optimization: the assumption is that having already shrunk the // transformation sequence down to minimal form, all transformations have a // role to play, and it's almost certainly a waste of time to invoke the // shrinker's interestingness function if we have eliminated transformations // that the shrinker previously tried to -- but could not -- eliminate. std::vector binary_out; protobufs::TransformationSequence modified_transformations; ReplayAdaptedTransformations(binary_under_reduction, &binary_out, &modified_transformations); if (transformation_sequence_in_.transformation_size() != modified_transformations.transformation_size()) { return false; } // The resulting binary must be deemed interesting according to the shrinker's // interestingness function. return shrinker_interestingness_function_( binary_out, counter_for_shrinker_interestingness_function); } void AddedFunctionReducer::ReplayPrefixAndAddFunction( std::vector* binary_out, std::unordered_set* irrelevant_pointee_global_variables) const { assert(transformation_sequence_in_ .transformation(index_of_add_function_transformation_) .has_add_function() && "A TransformationAddFunction is required at the given index."); auto replay_result = Replayer(target_env_, consumer_, binary_in_, initial_facts_, transformation_sequence_in_, index_of_add_function_transformation_, validate_during_replay_, validator_options_) .Run(); assert(replay_result.status == Replayer::ReplayerResultStatus::kComplete && "Replay should succeed"); assert(static_cast( replay_result.applied_transformations.transformation_size()) == index_of_add_function_transformation_ && "All requested transformations should have applied."); auto* ir_context = replay_result.transformed_module.get(); for (auto& type_or_value : ir_context->module()->types_values()) { if (type_or_value.opcode() != spv::Op::OpVariable) { continue; } if (replay_result.transformation_context->GetFactManager() ->PointeeValueIsIrrelevant(type_or_value.result_id())) { irrelevant_pointee_global_variables->insert(type_or_value.result_id()); } } // Add the function associated with the transformation at // |index_of_add_function_transformation| to the module. By construction this // should succeed. const protobufs::TransformationAddFunction& transformation_add_function_message = transformation_sequence_in_ .transformation(index_of_add_function_transformation_) .add_function(); bool success = TransformationAddFunction(transformation_add_function_message) .TryToAddFunction(ir_context); (void)success; // Keep release mode compilers happy. assert(success && "Addition of the function should have succeeded."); // Get the binary representation of the module with this function added. ir_context->module()->ToBinary(binary_out, false); } void AddedFunctionReducer::ReplayAdaptedTransformations( const std::vector& binary_under_reduction, std::vector* binary_out, protobufs::TransformationSequence* transformation_sequence_out) const { assert(index_of_add_function_transformation_ < static_cast( transformation_sequence_in_.transformation_size()) && "The relevant add function transformation must be present."); std::unique_ptr ir_context_under_reduction = BuildModule(target_env_, consumer_, binary_under_reduction.data(), binary_under_reduction.size()); assert(ir_context_under_reduction && "Error building module."); protobufs::TransformationSequence modified_transformations; for (uint32_t i = 0; i < static_cast(transformation_sequence_in_.transformation_size()); i++) { if (i == index_of_add_function_transformation_) { protobufs::TransformationAddFunction modified_add_function = transformation_sequence_in_ .transformation(index_of_add_function_transformation_) .add_function(); assert(GetAddedFunctionId() == modified_add_function.instruction(0).result_id() && "Unexpected result id for added function."); modified_add_function.clear_instruction(); for (auto& function : *ir_context_under_reduction->module()) { if (function.result_id() != GetAddedFunctionId()) { continue; } function.ForEachInst( [&modified_add_function](const opt::Instruction* instruction) { *modified_add_function.add_instruction() = MakeInstructionMessage(instruction); }); } assert(modified_add_function.instruction_size() > 0 && "Some instructions for the added function should remain."); *modified_transformations.add_transformation()->mutable_add_function() = modified_add_function; } else { *modified_transformations.add_transformation() = transformation_sequence_in_.transformation(i); } } assert( transformation_sequence_in_.transformation_size() == modified_transformations.transformation_size() && "The original and modified transformations should have the same size."); auto replay_result = Replayer(target_env_, consumer_, binary_in_, initial_facts_, modified_transformations, modified_transformations.transformation_size(), validate_during_replay_, validator_options_) .Run(); assert(replay_result.status == Replayer::ReplayerResultStatus::kComplete && "Replay should succeed."); replay_result.transformed_module->module()->ToBinary(binary_out, false); *transformation_sequence_out = std::move(replay_result.applied_transformations); } uint32_t AddedFunctionReducer::GetAddedFunctionId() const { return transformation_sequence_in_ .transformation(index_of_add_function_transformation_) .add_function() .instruction(0) .result_id(); } } // namespace fuzz } // namespace spvtools