// 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/transformation_duplicate_region_with_selection.h" #include "source/fuzz/fuzzer_util.h" namespace spvtools { namespace fuzz { TransformationDuplicateRegionWithSelection:: TransformationDuplicateRegionWithSelection( const spvtools::fuzz::protobufs:: TransformationDuplicateRegionWithSelection& message) : message_(message) {} TransformationDuplicateRegionWithSelection:: TransformationDuplicateRegionWithSelection( uint32_t new_entry_fresh_id, uint32_t condition_id, uint32_t merge_label_fresh_id, uint32_t entry_block_id, uint32_t exit_block_id, const std::map& original_label_to_duplicate_label, const std::map& original_id_to_duplicate_id, const std::map& original_id_to_phi_id) { message_.set_new_entry_fresh_id(new_entry_fresh_id); message_.set_condition_id(condition_id); message_.set_merge_label_fresh_id(merge_label_fresh_id); message_.set_entry_block_id(entry_block_id); message_.set_exit_block_id(exit_block_id); *message_.mutable_original_label_to_duplicate_label() = fuzzerutil::MapToRepeatedUInt32Pair(original_label_to_duplicate_label); *message_.mutable_original_id_to_duplicate_id() = fuzzerutil::MapToRepeatedUInt32Pair(original_id_to_duplicate_id); *message_.mutable_original_id_to_phi_id() = fuzzerutil::MapToRepeatedUInt32Pair(original_id_to_phi_id); } bool TransformationDuplicateRegionWithSelection::IsApplicable( opt::IRContext* ir_context, const TransformationContext& transformation_context) const { // Instruction with the id |condition_id| must exist and must be of a bool // type. auto bool_instr = ir_context->get_def_use_mgr()->GetDef(message_.condition_id()); if (bool_instr == nullptr || !bool_instr->type_id()) { return false; } if (!ir_context->get_type_mgr()->GetType(bool_instr->type_id())->AsBool()) { return false; } // The |new_entry_fresh_id| must be fresh and distinct. std::set ids_used_by_this_transformation; if (!CheckIdIsFreshAndNotUsedByThisTransformation( message_.new_entry_fresh_id(), ir_context, &ids_used_by_this_transformation)) { return false; } // The |merge_label_fresh_id| must be fresh and distinct. if (!CheckIdIsFreshAndNotUsedByThisTransformation( message_.merge_label_fresh_id(), ir_context, &ids_used_by_this_transformation)) { return false; } // The entry and exit block ids must refer to blocks. for (auto block_id : {message_.entry_block_id(), message_.exit_block_id()}) { auto block_label = ir_context->get_def_use_mgr()->GetDef(block_id); if (!block_label || block_label->opcode() != SpvOpLabel) { return false; } } auto entry_block = ir_context->cfg()->block(message_.entry_block_id()); auto exit_block = ir_context->cfg()->block(message_.exit_block_id()); // The |entry_block| and the |exit_block| must be in the same function. if (entry_block->GetParent() != exit_block->GetParent()) { return false; } // The |entry_block| must dominate the |exit_block|. auto dominator_analysis = ir_context->GetDominatorAnalysis(entry_block->GetParent()); if (!dominator_analysis->Dominates(entry_block, exit_block)) { return false; } // The |exit_block| must post-dominate the |entry_block|. auto postdominator_analysis = ir_context->GetPostDominatorAnalysis(entry_block->GetParent()); if (!postdominator_analysis->Dominates(exit_block, entry_block)) { return false; } auto enclosing_function = entry_block->GetParent(); // |entry_block| cannot be the first block of the |enclosing_function|. if (&*enclosing_function->begin() == entry_block) { return false; } // To make the process of resolving OpPhi instructions easier, we require that // the entry block has only one predecessor. auto entry_block_preds = ir_context->cfg()->preds(entry_block->id()); std::sort(entry_block_preds.begin(), entry_block_preds.end()); entry_block_preds.erase( std::unique(entry_block_preds.begin(), entry_block_preds.end()), entry_block_preds.end()); if (entry_block_preds.size() > 1) { return false; } // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3785): // The following code has been copied from TransformationOutlineFunction. // Consider refactoring to avoid duplication. auto region_set = GetRegionBlocks(ir_context, entry_block, exit_block); // Check whether |region_set| really is a single-entry single-exit region, and // also check whether structured control flow constructs and their merge // and continue constructs are either wholly in or wholly out of the region - // e.g. avoid the situation where the region contains the head of a loop but // not the loop's continue construct. // // This is achieved by going through every block in the |enclosing_function| for (auto& block : *enclosing_function) { if (&block == exit_block) { // It is not OK for the exit block to head a loop construct or a // conditional construct. if (block.GetMergeInst()) { return false; } continue; } if (region_set.count(&block) != 0) { // The block is in the region and is not the region's exit block. Let's // see whether all of the block's successors are in the region. If they // are not, the region is not single-entry single-exit. bool all_successors_in_region = true; block.WhileEachSuccessorLabel([&all_successors_in_region, ir_context, ®ion_set](uint32_t successor) -> bool { if (region_set.count(ir_context->cfg()->block(successor)) == 0) { all_successors_in_region = false; return false; } return true; }); if (!all_successors_in_region) { return false; } } if (auto merge = block.GetMergeInst()) { // The block is a loop or selection header. The header and its // associated merge block must be both in the region or both be // outside the region. auto merge_block = ir_context->cfg()->block(merge->GetSingleWordOperand(0)); if (region_set.count(&block) != region_set.count(merge_block)) { return false; } } if (auto loop_merge = block.GetLoopMergeInst()) { // The continue target of a loop must be within the region if and only if // the header of the loop is. auto continue_target = ir_context->cfg()->block(loop_merge->GetSingleWordOperand(1)); // The continue target is a single-entry, single-exit region. Therefore, // if the continue target is the exit block, the region might not contain // the loop header. However, we would like to exclude this situation, // since it would be impossible for the modified exit block to branch to // the new selection merge block. In this scenario the exit block is // required to branch to the loop header. if (region_set.count(&block) != region_set.count(continue_target)) { return false; } } } // Get the maps from the protobuf. std::map original_label_to_duplicate_label = fuzzerutil::RepeatedUInt32PairToMap( message_.original_label_to_duplicate_label()); std::map original_id_to_duplicate_id = fuzzerutil::RepeatedUInt32PairToMap( message_.original_id_to_duplicate_id()); std::map original_id_to_phi_id = fuzzerutil::RepeatedUInt32PairToMap(message_.original_id_to_phi_id()); for (auto block : region_set) { // The label of every block in the region must be present in the map // |original_label_to_duplicate_label|, unless overflow ids are present. if (original_label_to_duplicate_label.count(block->id()) == 0) { if (!transformation_context.GetOverflowIdSource()->HasOverflowIds()) { return false; } } else { auto duplicate_label = original_label_to_duplicate_label.at(block->id()); // Each id assigned to labels in the region must be distinct and fresh. if (!duplicate_label || !CheckIdIsFreshAndNotUsedByThisTransformation( duplicate_label, ir_context, &ids_used_by_this_transformation)) { return false; } } for (auto& instr : *block) { if (!instr.HasResultId()) { continue; } // Every instruction with a result id in the region must be present in the // map |original_id_to_duplicate_id|, unless overflow ids are present. if (original_id_to_duplicate_id.count(instr.result_id()) == 0) { if (!transformation_context.GetOverflowIdSource()->HasOverflowIds()) { return false; } } else { auto duplicate_id = original_id_to_duplicate_id.at(instr.result_id()); // Id assigned to this result id in the region must be distinct and // fresh. if (!duplicate_id || !CheckIdIsFreshAndNotUsedByThisTransformation( duplicate_id, ir_context, &ids_used_by_this_transformation)) { return false; } } // If the instruction is available at the end of the region then we would // like to be able to add an OpPhi instruction at the merge point of the // duplicated region to capture the values computed by both duplicates of // the instruction, so that this is also available after the region. We // do this not just for instructions that are already used after the // region, but for all instructions so that the phi is available to future // transformations. if (AvailableAfterRegion(instr, exit_block, ir_context)) { if (!ValidOpPhiArgument(instr, ir_context)) { // The instruction cannot be used as an OpPhi argument. This is a // blocker if there are uses of the instruction after the region. // Otherwise we can simply avoid generating an OpPhi for this // instruction and its duplicate. if (!ir_context->get_def_use_mgr()->WhileEachUser( &instr, [ir_context, ®ion_set](opt::Instruction* use_instr) -> bool { opt::BasicBlock* use_block = ir_context->get_instr_block(use_instr); return use_block == nullptr || region_set.count(use_block) > 0; })) { return false; } } else { // Every instruction with a result id available at the end of the // region must be present in the map |original_id_to_phi_id|, unless // overflow ids are present. if (original_id_to_phi_id.count(instr.result_id()) == 0) { if (!transformation_context.GetOverflowIdSource() ->HasOverflowIds()) { return false; } } else { auto phi_id = original_id_to_phi_id.at(instr.result_id()); // Id assigned to this result id in the region must be distinct and // fresh. if (!phi_id || !CheckIdIsFreshAndNotUsedByThisTransformation( phi_id, ir_context, &ids_used_by_this_transformation)) { return false; } } } } } } return true; } void TransformationDuplicateRegionWithSelection::Apply( opt::IRContext* ir_context, TransformationContext* transformation_context) const { fuzzerutil::UpdateModuleIdBound(ir_context, message_.new_entry_fresh_id()); fuzzerutil::UpdateModuleIdBound(ir_context, message_.merge_label_fresh_id()); // Create the new entry block containing the main conditional instruction. Set // its parent to the parent of the original entry block, since it is located // in the same function. std::unique_ptr new_entry_block = MakeUnique(MakeUnique( ir_context, SpvOpLabel, 0, message_.new_entry_fresh_id(), opt::Instruction::OperandList())); auto entry_block = ir_context->cfg()->block(message_.entry_block_id()); auto enclosing_function = entry_block->GetParent(); auto exit_block = ir_context->cfg()->block(message_.exit_block_id()); // Get the blocks contained in the region. std::set region_blocks = GetRegionBlocks(ir_context, entry_block, exit_block); // Construct the merge block. std::unique_ptr merge_block = MakeUnique(MakeUnique(opt::Instruction( ir_context, SpvOpLabel, 0, message_.merge_label_fresh_id(), opt::Instruction::OperandList()))); // Get the maps from the protobuf. std::map original_label_to_duplicate_label = fuzzerutil::RepeatedUInt32PairToMap( message_.original_label_to_duplicate_label()); std::map original_id_to_duplicate_id = fuzzerutil::RepeatedUInt32PairToMap( message_.original_id_to_duplicate_id()); std::map original_id_to_phi_id = fuzzerutil::RepeatedUInt32PairToMap(message_.original_id_to_phi_id()); // Use oveflow ids to fill in any required ids that are missing from these // maps. for (auto block : region_blocks) { if (original_label_to_duplicate_label.count(block->id()) == 0) { original_label_to_duplicate_label.insert( {block->id(), transformation_context->GetOverflowIdSource()->GetNextOverflowId()}); } for (auto& instr : *block) { if (!instr.HasResultId()) { continue; } if (original_id_to_duplicate_id.count(instr.result_id()) == 0) { original_id_to_duplicate_id.insert( {instr.result_id(), transformation_context->GetOverflowIdSource() ->GetNextOverflowId()}); } if (AvailableAfterRegion(instr, exit_block, ir_context) && ValidOpPhiArgument(instr, ir_context)) { if (original_id_to_phi_id.count(instr.result_id()) == 0) { original_id_to_phi_id.insert( {instr.result_id(), transformation_context->GetOverflowIdSource() ->GetNextOverflowId()}); } } } } // Before adding duplicate blocks, we need to update the OpPhi instructions in // the successors of the |exit_block|. We know that the execution of the // transformed region will end in |merge_block|. Hence, we need to change all // occurrences of the label id of the |exit_block| to the label id of the // |merge_block|. exit_block->ForEachSuccessorLabel([this, ir_context](uint32_t label_id) { auto block = ir_context->cfg()->block(label_id); for (auto& instr : *block) { if (instr.opcode() == SpvOpPhi) { instr.ForEachId([this](uint32_t* id) { if (*id == message_.exit_block_id()) { *id = message_.merge_label_fresh_id(); } }); } } }); // Get vector of predecessors id of |entry_block|. Remove any duplicate // values. auto entry_block_preds = ir_context->cfg()->preds(entry_block->id()); std::sort(entry_block_preds.begin(), entry_block_preds.end()); entry_block_preds.erase( unique(entry_block_preds.begin(), entry_block_preds.end()), entry_block_preds.end()); // We know that |entry_block| has only one predecessor, since the region is // single-entry, single-exit and its constructs and their merge blocks must be // either wholly within or wholly outside of the region. assert(entry_block_preds.size() == 1 && "The entry of the region to be duplicated can have only one " "predecessor."); uint32_t entry_block_pred_id = ir_context->get_instr_block(entry_block_preds[0])->id(); // Update all the OpPhi instructions in the |entry_block|. Change every // occurrence of |entry_block_pred_id| to the id of |new_entry|, because we // will insert |new_entry| before |entry_block|. for (auto& instr : *entry_block) { if (instr.opcode() == SpvOpPhi) { instr.ForEachId([this, entry_block_pred_id](uint32_t* id) { if (*id == entry_block_pred_id) { *id = message_.new_entry_fresh_id(); } }); } } // Duplication of blocks will invalidate iterators. Store all the blocks from // the enclosing function. std::vector blocks; for (auto& block : *enclosing_function) { blocks.push_back(&block); } opt::BasicBlock* previous_block = nullptr; opt::BasicBlock* duplicated_exit_block = nullptr; // Iterate over all blocks of the function to duplicate blocks of the original // region and their instructions. for (auto& block : blocks) { // The block must be contained in the region. if (region_blocks.count(block) == 0) { continue; } fuzzerutil::UpdateModuleIdBound( ir_context, original_label_to_duplicate_label.at(block->id())); std::unique_ptr duplicated_block = MakeUnique(MakeUnique( ir_context, SpvOpLabel, 0, original_label_to_duplicate_label.at(block->id()), opt::Instruction::OperandList())); for (auto& instr : *block) { // Case where an instruction is the terminator of the exit block is // handled separately. if (block == exit_block && instr.IsBlockTerminator()) { switch (instr.opcode()) { case SpvOpBranch: case SpvOpBranchConditional: case SpvOpReturn: case SpvOpReturnValue: case SpvOpUnreachable: case SpvOpKill: continue; default: assert(false && "Unexpected terminator for |exit_block| of the region."); } } // Duplicate the instruction. auto cloned_instr = instr.Clone(ir_context); duplicated_block->AddInstruction( std::unique_ptr(cloned_instr)); if (instr.HasResultId()) { fuzzerutil::UpdateModuleIdBound( ir_context, original_id_to_duplicate_id.at(instr.result_id())); } // If an id from the original region was used in this instruction, // replace it with the value from |original_id_to_duplicate_id|. // If a label from the original region was used in this instruction, // replace it with the value from |original_label_to_duplicate_label|. cloned_instr->ForEachId( [original_id_to_duplicate_id, original_label_to_duplicate_label](uint32_t* op) { if (original_id_to_duplicate_id.count(*op) != 0) { *op = original_id_to_duplicate_id.at(*op); } else if (original_label_to_duplicate_label.count(*op) != 0) { *op = original_label_to_duplicate_label.at(*op); } }); } // If the block is the first duplicated block, insert it after the exit // block of the original region. Otherwise, insert it after the preceding // one. auto duplicated_block_ptr = duplicated_block.get(); if (previous_block) { enclosing_function->InsertBasicBlockAfter(std::move(duplicated_block), previous_block); } else { enclosing_function->InsertBasicBlockAfter(std::move(duplicated_block), exit_block); } previous_block = duplicated_block_ptr; if (block == exit_block) { // After execution of the loop, this variable stores a pointer to the last // duplicated block. duplicated_exit_block = duplicated_block_ptr; } } for (auto& block : region_blocks) { for (auto& instr : *block) { if (instr.result_id() == 0) { continue; } if (AvailableAfterRegion(instr, exit_block, ir_context) && ValidOpPhiArgument(instr, ir_context)) { // Add an OpPhi instruction for every result id that is available at // the end of the region, as long as the result id is valid for use // with OpPhi. merge_block->AddInstruction(MakeUnique( ir_context, SpvOpPhi, instr.type_id(), original_id_to_phi_id.at(instr.result_id()), opt::Instruction::OperandList({ {SPV_OPERAND_TYPE_ID, {instr.result_id()}}, {SPV_OPERAND_TYPE_ID, {exit_block->id()}}, {SPV_OPERAND_TYPE_ID, {original_id_to_duplicate_id.at(instr.result_id())}}, {SPV_OPERAND_TYPE_ID, {duplicated_exit_block->id()}}, }))); fuzzerutil::UpdateModuleIdBound( ir_context, original_id_to_phi_id.at(instr.result_id())); // If the instruction has been remapped by an OpPhi, look // for all its uses outside of the region and outside of the // merge block (to not overwrite just added instructions in // the merge block) and replace the original instruction id // with the id of the corresponding OpPhi instruction. ir_context->get_def_use_mgr()->ForEachUse( &instr, [ir_context, &instr, region_blocks, original_id_to_phi_id, &merge_block](opt::Instruction* user, uint32_t operand_index) { auto user_block = ir_context->get_instr_block(user); if ((region_blocks.find(user_block) != region_blocks.end()) || user_block == merge_block.get()) { return; } user->SetOperand(operand_index, {original_id_to_phi_id.at(instr.result_id())}); }); } } } // Construct a conditional instruction in the |new_entry_block|. // If the condition is true, the execution proceeds in the // |entry_block| of the original region. If the condition is // false, the execution proceeds in the first block of the // duplicated region. new_entry_block->AddInstruction(MakeUnique( ir_context, SpvOpSelectionMerge, 0, 0, opt::Instruction::OperandList( {{SPV_OPERAND_TYPE_ID, {message_.merge_label_fresh_id()}}, {SPV_OPERAND_TYPE_SELECTION_CONTROL, {SpvSelectionControlMaskNone}}}))); new_entry_block->AddInstruction(MakeUnique( ir_context, SpvOpBranchConditional, 0, 0, opt::Instruction::OperandList( {{SPV_OPERAND_TYPE_ID, {message_.condition_id()}}, {SPV_OPERAND_TYPE_ID, {message_.entry_block_id()}}, {SPV_OPERAND_TYPE_ID, {original_label_to_duplicate_label.at( message_.entry_block_id())}}}))); // Move the terminator of |exit_block| to the end of // |merge_block|. auto exit_block_terminator = exit_block->terminator(); auto cloned_instr = exit_block_terminator->Clone(ir_context); merge_block->AddInstruction(std::unique_ptr(cloned_instr)); ir_context->KillInst(exit_block_terminator); // Add OpBranch instruction to the merge block at the end of // |exit_block| and at the end of |duplicated_exit_block|, so that // the execution proceeds in the |merge_block|. opt::Instruction merge_branch_instr = opt::Instruction( ir_context, SpvOpBranch, 0, 0, opt::Instruction::OperandList( {{SPV_OPERAND_TYPE_ID, {message_.merge_label_fresh_id()}}})); exit_block->AddInstruction(MakeUnique(merge_branch_instr)); duplicated_exit_block->AddInstruction( std::unique_ptr(merge_branch_instr.Clone(ir_context))); // Execution needs to start in the |new_entry_block|. Change all // the uses of |entry_block_label_instr| outside of the original // region to |message_.new_entry_fresh_id|. auto entry_block_label_instr = ir_context->get_def_use_mgr()->GetDef(message_.entry_block_id()); ir_context->get_def_use_mgr()->ForEachUse( entry_block_label_instr, [this, ir_context, region_blocks](opt::Instruction* user, uint32_t operand_index) { auto user_block = ir_context->get_instr_block(user); if ((region_blocks.count(user_block) != 0)) { return; } switch (user->opcode()) { case SpvOpSwitch: case SpvOpBranch: case SpvOpBranchConditional: case SpvOpLoopMerge: case SpvOpSelectionMerge: { user->SetOperand(operand_index, {message_.new_entry_fresh_id()}); } break; case SpvOpName: break; default: assert(false && "The label id cannot be used by instructions " "other than " "OpSwitch, OpBranch, OpBranchConditional, " "OpLoopMerge, " "OpSelectionMerge"); } }); opt::Instruction* merge_block_terminator = merge_block->terminator(); switch (merge_block_terminator->opcode()) { case SpvOpReturnValue: case SpvOpBranchConditional: { uint32_t operand = merge_block_terminator->GetSingleWordInOperand(0); if (original_id_to_phi_id.count(operand)) { merge_block_terminator->SetInOperand( 0, {original_id_to_phi_id.at(operand)}); } break; } default: break; } // Insert the merge block after the |duplicated_exit_block| (the // last duplicated block). enclosing_function->InsertBasicBlockAfter(std::move(merge_block), duplicated_exit_block); // Insert the |new_entry_block| before the entry block of the // original region. enclosing_function->InsertBasicBlockBefore(std::move(new_entry_block), entry_block); // Since we have changed the module, most of the analysis are now // invalid. We can invalidate analyses now after all of the blocks // have been registered. ir_context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone); } // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3785): // The following method has been copied from // TransformationOutlineFunction. Consider refactoring to avoid // duplication. std::set TransformationDuplicateRegionWithSelection::GetRegionBlocks( opt::IRContext* ir_context, opt::BasicBlock* entry_block, opt::BasicBlock* exit_block) { auto enclosing_function = entry_block->GetParent(); auto dominator_analysis = ir_context->GetDominatorAnalysis(enclosing_function); auto postdominator_analysis = ir_context->GetPostDominatorAnalysis(enclosing_function); // A block belongs to a region between the entry block and the exit // block if and only if it is dominated by the entry block and // post-dominated by the exit block. std::set result; for (auto& block : *enclosing_function) { if (dominator_analysis->Dominates(entry_block, &block) && postdominator_analysis->Dominates(exit_block, &block)) { result.insert(&block); } } return result; } protobufs::Transformation TransformationDuplicateRegionWithSelection::ToMessage() const { protobufs::Transformation result; *result.mutable_duplicate_region_with_selection() = message_; return result; } std::unordered_set TransformationDuplicateRegionWithSelection::GetFreshIds() const { std::unordered_set result = {message_.new_entry_fresh_id(), message_.merge_label_fresh_id()}; for (auto& pair : message_.original_label_to_duplicate_label()) { result.insert(pair.second()); } for (auto& pair : message_.original_id_to_duplicate_id()) { result.insert(pair.second()); } for (auto& pair : message_.original_id_to_phi_id()) { result.insert(pair.second()); } return result; } bool TransformationDuplicateRegionWithSelection::AvailableAfterRegion( const opt::Instruction& instr, opt::BasicBlock* exit_block, opt::IRContext* ir_context) { opt::Instruction* final_instruction_in_region = &*exit_block->tail(); return &instr == final_instruction_in_region || fuzzerutil::IdIsAvailableBeforeInstruction( ir_context, final_instruction_in_region, instr.result_id()); } bool TransformationDuplicateRegionWithSelection::ValidOpPhiArgument( const opt::Instruction& instr, opt::IRContext* ir_context) { opt::Instruction* instr_type = ir_context->get_def_use_mgr()->GetDef(instr.type_id()); // It is invalid to apply OpPhi to void-typed values. if (instr_type->opcode() == SpvOpTypeVoid) { return false; } // Using pointers with OpPhi requires capability VariablePointers. if (instr_type->opcode() == SpvOpTypePointer && !ir_context->get_feature_mgr()->HasCapability( SpvCapabilityVariablePointers)) { return false; } // OpTypeSampledImage cannot be the result type of an OpPhi instruction. if (instr_type->opcode() == SpvOpTypeSampledImage) { return false; } return true; } } // namespace fuzz } // namespace spvtools