| // Copyright (c) 2020 André Perez Maselco | |
| // | |
| // 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_inline_function.h" | |
| #include "source/fuzz/fuzzer_util.h" | |
| #include "source/fuzz/instruction_descriptor.h" | |
| namespace spvtools { | |
| namespace fuzz { | |
| TransformationInlineFunction::TransformationInlineFunction( | |
| protobufs::TransformationInlineFunction message) | |
| : message_(std::move(message)) {} | |
| TransformationInlineFunction::TransformationInlineFunction( | |
| uint32_t function_call_id, | |
| const std::map<uint32_t, uint32_t>& result_id_map) { | |
| message_.set_function_call_id(function_call_id); | |
| *message_.mutable_result_id_map() = | |
| fuzzerutil::MapToRepeatedUInt32Pair(result_id_map); | |
| } | |
| bool TransformationInlineFunction::IsApplicable( | |
| opt::IRContext* ir_context, | |
| const TransformationContext& transformation_context) const { | |
| // The values in the |message_.result_id_map| must be all fresh and all | |
| // distinct. | |
| const auto result_id_map = | |
| fuzzerutil::RepeatedUInt32PairToMap(message_.result_id_map()); | |
| std::set<uint32_t> ids_used_by_this_transformation; | |
| for (auto& pair : result_id_map) { | |
| if (!CheckIdIsFreshAndNotUsedByThisTransformation( | |
| pair.second, ir_context, &ids_used_by_this_transformation)) { | |
| return false; | |
| } | |
| } | |
| // |function_call_instruction| must be suitable for inlining. | |
| auto* function_call_instruction = | |
| ir_context->get_def_use_mgr()->GetDef(message_.function_call_id()); | |
| if (!IsSuitableForInlining(ir_context, function_call_instruction)) { | |
| return false; | |
| } | |
| // |function_call_instruction| must be the penultimate instruction in its | |
| // block and its block termination instruction must be an OpBranch. This | |
| // avoids the case where the penultimate instruction is an OpLoopMerge, which | |
| // would make the back-edge block not branch to the loop header. | |
| auto* function_call_instruction_block = | |
| ir_context->get_instr_block(function_call_instruction); | |
| if (function_call_instruction != | |
| &*--function_call_instruction_block->tail() || | |
| function_call_instruction_block->terminator()->opcode() != | |
| spv::Op::OpBranch) { | |
| return false; | |
| } | |
| auto* called_function = fuzzerutil::FindFunction( | |
| ir_context, function_call_instruction->GetSingleWordInOperand(0)); | |
| for (auto& block : *called_function) { | |
| // Since the entry block label will not be inlined, only the remaining | |
| // labels must have a corresponding value in the map. | |
| if (&block != &*called_function->entry() && | |
| !result_id_map.count(block.id()) && | |
| !transformation_context.GetOverflowIdSource()->HasOverflowIds()) { | |
| return false; | |
| } | |
| // |result_id_map| must have an entry for every result id in the called | |
| // function. | |
| for (auto& instruction : block) { | |
| // If |instruction| has result id, then it must have a mapped id in | |
| // |result_id_map|. | |
| if (instruction.HasResultId() && | |
| !result_id_map.count(instruction.result_id()) && | |
| !transformation_context.GetOverflowIdSource()->HasOverflowIds()) { | |
| return false; | |
| } | |
| } | |
| } | |
| // |result_id_map| must not contain an entry for any parameter of the function | |
| // that is being inlined. | |
| bool found_entry_for_parameter = false; | |
| called_function->ForEachParam( | |
| [&result_id_map, &found_entry_for_parameter](opt::Instruction* param) { | |
| if (result_id_map.count(param->result_id())) { | |
| found_entry_for_parameter = true; | |
| } | |
| }); | |
| return !found_entry_for_parameter; | |
| } | |
| void TransformationInlineFunction::Apply( | |
| opt::IRContext* ir_context, | |
| TransformationContext* transformation_context) const { | |
| auto* function_call_instruction = | |
| ir_context->get_def_use_mgr()->GetDef(message_.function_call_id()); | |
| auto* caller_function = | |
| ir_context->get_instr_block(function_call_instruction)->GetParent(); | |
| auto* called_function = fuzzerutil::FindFunction( | |
| ir_context, function_call_instruction->GetSingleWordInOperand(0)); | |
| std::map<uint32_t, uint32_t> result_id_map = | |
| fuzzerutil::RepeatedUInt32PairToMap(message_.result_id_map()); | |
| // If there are gaps in the result id map, fill them using overflow ids. | |
| for (auto& block : *called_function) { | |
| if (&block != &*called_function->entry() && | |
| !result_id_map.count(block.id())) { | |
| result_id_map.insert( | |
| {block.id(), | |
| transformation_context->GetOverflowIdSource()->GetNextOverflowId()}); | |
| } | |
| for (auto& instruction : block) { | |
| // If |instruction| has result id, then it must have a mapped id in | |
| // |result_id_map|. | |
| if (instruction.HasResultId() && | |
| !result_id_map.count(instruction.result_id())) { | |
| result_id_map.insert({instruction.result_id(), | |
| transformation_context->GetOverflowIdSource() | |
| ->GetNextOverflowId()}); | |
| } | |
| } | |
| } | |
| auto* successor_block = ir_context->cfg()->block( | |
| ir_context->get_instr_block(function_call_instruction) | |
| ->terminator() | |
| ->GetSingleWordInOperand(0)); | |
| // Inline the |called_function| entry block. | |
| for (auto& entry_block_instruction : *called_function->entry()) { | |
| opt::Instruction* inlined_instruction; | |
| if (entry_block_instruction.opcode() == spv::Op::OpVariable) { | |
| // All OpVariable instructions in a function must be in the first block | |
| // in the function. | |
| inlined_instruction = caller_function->begin()->begin()->InsertBefore( | |
| std::unique_ptr<opt::Instruction>( | |
| entry_block_instruction.Clone(ir_context))); | |
| } else { | |
| inlined_instruction = function_call_instruction->InsertBefore( | |
| std::unique_ptr<opt::Instruction>( | |
| entry_block_instruction.Clone(ir_context))); | |
| } | |
| AdaptInlinedInstruction(result_id_map, ir_context, inlined_instruction); | |
| } | |
| // If the function call's successor block contains OpPhi instructions that | |
| // refer to the block containing the call then these will need to be rewritten | |
| // to instead refer to the block associated with "returning" from the inlined | |
| // function, as this block will be the predecessor of what used to be the | |
| // function call's successor block. We look out for this block. | |
| uint32_t new_return_block_id = 0; | |
| // Inline the |called_function| non-entry blocks. | |
| for (auto& block : *called_function) { | |
| if (&block == &*called_function->entry()) { | |
| continue; | |
| } | |
| // Check whether this is the function's return block. Take note if it is, | |
| // so that OpPhi instructions in the successor of the original function call | |
| // block can be re-written. | |
| if (block.terminator()->IsReturn()) { | |
| assert(new_return_block_id == 0 && | |
| "There should be only one return block."); | |
| new_return_block_id = result_id_map.at(block.id()); | |
| } | |
| auto* cloned_block = block.Clone(ir_context); | |
| cloned_block = caller_function->InsertBasicBlockBefore( | |
| std::unique_ptr<opt::BasicBlock>(cloned_block), successor_block); | |
| cloned_block->GetLabel()->SetResultId(result_id_map.at(cloned_block->id())); | |
| fuzzerutil::UpdateModuleIdBound(ir_context, cloned_block->id()); | |
| for (auto& inlined_instruction : *cloned_block) { | |
| AdaptInlinedInstruction(result_id_map, ir_context, &inlined_instruction); | |
| } | |
| } | |
| opt::BasicBlock* block_containing_function_call = | |
| ir_context->get_instr_block(function_call_instruction); | |
| assert(((new_return_block_id == 0) == | |
| called_function->entry()->terminator()->IsReturn()) && | |
| "We should have found a return block unless the function being " | |
| "inlined returns in its first block."); | |
| if (new_return_block_id != 0) { | |
| // Rewrite any OpPhi instructions in the successor block so that they refer | |
| // to the new return block instead of the block that originally contained | |
| // the function call. | |
| ir_context->get_def_use_mgr()->ForEachUse( | |
| block_containing_function_call->id(), | |
| [ir_context, new_return_block_id, successor_block]( | |
| opt::Instruction* use_instruction, uint32_t operand_index) { | |
| if (use_instruction->opcode() == spv::Op::OpPhi && | |
| ir_context->get_instr_block(use_instruction) == successor_block) { | |
| use_instruction->SetOperand(operand_index, {new_return_block_id}); | |
| } | |
| }); | |
| } | |
| // Removes the function call instruction and its block termination instruction | |
| // from |caller_function|. | |
| ir_context->KillInst(block_containing_function_call->terminator()); | |
| ir_context->KillInst(function_call_instruction); | |
| // Since the SPIR-V module has changed, no analyses must be validated. | |
| ir_context->InvalidateAnalysesExceptFor( | |
| opt::IRContext::Analysis::kAnalysisNone); | |
| } | |
| protobufs::Transformation TransformationInlineFunction::ToMessage() const { | |
| protobufs::Transformation result; | |
| *result.mutable_inline_function() = message_; | |
| return result; | |
| } | |
| bool TransformationInlineFunction::IsSuitableForInlining( | |
| opt::IRContext* ir_context, opt::Instruction* function_call_instruction) { | |
| // |function_call_instruction| must be defined and must be an OpFunctionCall | |
| // instruction. | |
| if (!function_call_instruction || | |
| function_call_instruction->opcode() != spv::Op::OpFunctionCall) { | |
| return false; | |
| } | |
| // If |function_call_instruction| return type is void, then | |
| // |function_call_instruction| must not have uses. | |
| if (ir_context->get_type_mgr() | |
| ->GetType(function_call_instruction->type_id()) | |
| ->AsVoid() && | |
| ir_context->get_def_use_mgr()->NumUses(function_call_instruction) != 0) { | |
| return false; | |
| } | |
| // |called_function| must not have an early return. | |
| auto called_function = fuzzerutil::FindFunction( | |
| ir_context, function_call_instruction->GetSingleWordInOperand(0)); | |
| if (called_function->HasEarlyReturn()) { | |
| return false; | |
| } | |
| // |called_function| must not use OpKill or OpUnreachable. | |
| if (fuzzerutil::FunctionContainsOpKillOrUnreachable(*called_function)) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| void TransformationInlineFunction::AdaptInlinedInstruction( | |
| const std::map<uint32_t, uint32_t>& result_id_map, | |
| opt::IRContext* ir_context, | |
| opt::Instruction* instruction_to_be_inlined) const { | |
| auto* function_call_instruction = | |
| ir_context->get_def_use_mgr()->GetDef(message_.function_call_id()); | |
| auto* called_function = fuzzerutil::FindFunction( | |
| ir_context, function_call_instruction->GetSingleWordInOperand(0)); | |
| const auto* function_call_block = | |
| ir_context->get_instr_block(function_call_instruction); | |
| assert(function_call_block && "OpFunctionCall must belong to some block"); | |
| // Replaces the operand ids with their mapped result ids. | |
| instruction_to_be_inlined->ForEachInId( | |
| [called_function, function_call_instruction, &result_id_map, | |
| function_call_block](uint32_t* id) { | |
| // We are not inlining the entry block of the |called_function|. | |
| // | |
| // We must check this condition first since we can't use the fresh id | |
| // from |result_id_map| even if it has one. This is because that fresh | |
| // id will never be added to the module since entry blocks are not | |
| // inlined. | |
| if (*id == called_function->entry()->id()) { | |
| *id = function_call_block->id(); | |
| return; | |
| } | |
| // If |id| is mapped, then set it to its mapped value. | |
| if (result_id_map.count(*id)) { | |
| *id = result_id_map.at(*id); | |
| return; | |
| } | |
| uint32_t parameter_index = 0; | |
| called_function->ForEachParam( | |
| [id, function_call_instruction, | |
| ¶meter_index](opt::Instruction* parameter_instruction) { | |
| // If the id is a function parameter, then set it to the | |
| // parameter value passed in the function call instruction. | |
| if (*id == parameter_instruction->result_id()) { | |
| // We do + 1 because the first in-operand for OpFunctionCall is | |
| // the function id that is being called. | |
| *id = function_call_instruction->GetSingleWordInOperand( | |
| parameter_index + 1); | |
| } | |
| parameter_index++; | |
| }); | |
| }); | |
| // If |instruction_to_be_inlined| has result id, then set it to its mapped | |
| // value. | |
| if (instruction_to_be_inlined->HasResultId()) { | |
| assert(result_id_map.count(instruction_to_be_inlined->result_id()) && | |
| "Result id must be mapped to a fresh id."); | |
| instruction_to_be_inlined->SetResultId( | |
| result_id_map.at(instruction_to_be_inlined->result_id())); | |
| fuzzerutil::UpdateModuleIdBound(ir_context, | |
| instruction_to_be_inlined->result_id()); | |
| } | |
| // The return instruction will be changed into an OpBranch to the basic | |
| // block that follows the block containing the function call. | |
| if (spvOpcodeIsReturn(instruction_to_be_inlined->opcode())) { | |
| uint32_t successor_block_id = | |
| ir_context->get_instr_block(function_call_instruction) | |
| ->terminator() | |
| ->GetSingleWordInOperand(0); | |
| switch (instruction_to_be_inlined->opcode()) { | |
| case spv::Op::OpReturn: | |
| instruction_to_be_inlined->AddOperand( | |
| {SPV_OPERAND_TYPE_ID, {successor_block_id}}); | |
| break; | |
| case spv::Op::OpReturnValue: { | |
| instruction_to_be_inlined->InsertBefore(MakeUnique<opt::Instruction>( | |
| ir_context, spv::Op::OpCopyObject, | |
| function_call_instruction->type_id(), | |
| function_call_instruction->result_id(), | |
| opt::Instruction::OperandList( | |
| {{SPV_OPERAND_TYPE_ID, | |
| {instruction_to_be_inlined->GetSingleWordOperand(0)}}}))); | |
| instruction_to_be_inlined->SetInOperand(0, {successor_block_id}); | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| instruction_to_be_inlined->SetOpcode(spv::Op::OpBranch); | |
| } | |
| } | |
| std::unordered_set<uint32_t> TransformationInlineFunction::GetFreshIds() const { | |
| std::unordered_set<uint32_t> result; | |
| for (auto& pair : message_.result_id_map()) { | |
| result.insert(pair.second()); | |
| } | |
| return result; | |
| } | |
| } // namespace fuzz | |
| } // namespace spvtools |