| // Copyright (c) 2019 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/fuzzer_pass_obfuscate_constants.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "source/fuzz/fuzzer_util.h" |
| #include "source/fuzz/instruction_descriptor.h" |
| #include "source/fuzz/transformation_replace_boolean_constant_with_constant_binary.h" |
| #include "source/fuzz/transformation_replace_constant_with_uniform.h" |
| #include "source/fuzz/uniform_buffer_element_descriptor.h" |
| #include "source/opt/ir_context.h" |
| |
| namespace spvtools { |
| namespace fuzz { |
| |
| FuzzerPassObfuscateConstants::FuzzerPassObfuscateConstants( |
| opt::IRContext* ir_context, TransformationContext* transformation_context, |
| FuzzerContext* fuzzer_context, |
| protobufs::TransformationSequence* transformations, |
| bool ignore_inapplicable_transformations) |
| : FuzzerPass(ir_context, transformation_context, fuzzer_context, |
| transformations, ignore_inapplicable_transformations) {} |
| |
| void FuzzerPassObfuscateConstants::ObfuscateBoolConstantViaConstantPair( |
| uint32_t depth, const protobufs::IdUseDescriptor& bool_constant_use, |
| const std::vector<spv::Op>& greater_than_opcodes, |
| const std::vector<spv::Op>& less_than_opcodes, uint32_t constant_id_1, |
| uint32_t constant_id_2, bool first_constant_is_larger) { |
| auto bool_constant_opcode = GetIRContext() |
| ->get_def_use_mgr() |
| ->GetDef(bool_constant_use.id_of_interest()) |
| ->opcode(); |
| assert((bool_constant_opcode == spv::Op::OpConstantFalse || |
| bool_constant_opcode == spv::Op::OpConstantTrue) && |
| "Precondition: this must be a usage of a boolean constant."); |
| |
| // Pick an opcode at random. First randomly decide whether to generate |
| // a 'greater than' or 'less than' kind of opcode, and then select a |
| // random opcode from the resulting subset. |
| spv::Op comparison_opcode; |
| if (GetFuzzerContext()->ChooseEven()) { |
| comparison_opcode = greater_than_opcodes[GetFuzzerContext()->RandomIndex( |
| greater_than_opcodes)]; |
| } else { |
| comparison_opcode = |
| less_than_opcodes[GetFuzzerContext()->RandomIndex(less_than_opcodes)]; |
| } |
| |
| // We now need to decide how to order constant_id_1 and constant_id_2 such |
| // that 'constant_id_1 comparison_opcode constant_id_2' evaluates to the |
| // boolean constant. |
| const bool is_greater_than_opcode = |
| std::find(greater_than_opcodes.begin(), greater_than_opcodes.end(), |
| comparison_opcode) != greater_than_opcodes.end(); |
| uint32_t lhs_id; |
| uint32_t rhs_id; |
| if ((bool_constant_opcode == spv::Op::OpConstantTrue && |
| first_constant_is_larger == is_greater_than_opcode) || |
| (bool_constant_opcode == spv::Op::OpConstantFalse && |
| first_constant_is_larger != is_greater_than_opcode)) { |
| lhs_id = constant_id_1; |
| rhs_id = constant_id_2; |
| } else { |
| lhs_id = constant_id_2; |
| rhs_id = constant_id_1; |
| } |
| |
| // We can now make a transformation that will replace |bool_constant_use| |
| // with an expression of the form (written using infix notation): |
| // |lhs_id| |comparison_opcode| |rhs_id| |
| auto transformation = TransformationReplaceBooleanConstantWithConstantBinary( |
| bool_constant_use, lhs_id, rhs_id, comparison_opcode, |
| GetFuzzerContext()->GetFreshId()); |
| // The transformation should be applicable by construction. |
| assert( |
| transformation.IsApplicable(GetIRContext(), *GetTransformationContext())); |
| |
| // Applying this transformation yields a pointer to the new instruction that |
| // computes the result of the binary expression. |
| auto binary_operator_instruction = transformation.ApplyWithResult( |
| GetIRContext(), GetTransformationContext()); |
| |
| // Add this transformation to the sequence of transformations that have been |
| // applied. |
| *GetTransformations()->add_transformation() = transformation.ToMessage(); |
| |
| // Having made a binary expression, there may now be opportunities to further |
| // obfuscate the constants used as the LHS and RHS of the expression (e.g. by |
| // replacing them with loads from known uniforms). |
| // |
| // We thus consider operands 0 and 1 (LHS and RHS in turn). |
| for (uint32_t index : {0u, 1u}) { |
| // We randomly decide, based on the current depth of obfuscation, whether |
| // to further obfuscate this operand. |
| if (GetFuzzerContext()->GoDeeperInConstantObfuscation(depth)) { |
| auto in_operand_use = MakeIdUseDescriptor( |
| binary_operator_instruction->GetSingleWordInOperand(index), |
| MakeInstructionDescriptor(binary_operator_instruction->result_id(), |
| binary_operator_instruction->opcode(), 0), |
| index); |
| ObfuscateConstant(depth + 1, in_operand_use); |
| } |
| } |
| } |
| |
| void FuzzerPassObfuscateConstants::ObfuscateBoolConstantViaFloatConstantPair( |
| uint32_t depth, const protobufs::IdUseDescriptor& bool_constant_use, |
| uint32_t float_constant_id_1, uint32_t float_constant_id_2) { |
| auto float_constant_1 = GetIRContext() |
| ->get_constant_mgr() |
| ->FindDeclaredConstant(float_constant_id_1) |
| ->AsFloatConstant(); |
| auto float_constant_2 = GetIRContext() |
| ->get_constant_mgr() |
| ->FindDeclaredConstant(float_constant_id_2) |
| ->AsFloatConstant(); |
| assert(float_constant_1->words() != float_constant_2->words() && |
| "The constants should not be identical."); |
| assert(std::isfinite(float_constant_1->GetValueAsDouble()) && |
| "The constants must be finite numbers."); |
| assert(std::isfinite(float_constant_2->GetValueAsDouble()) && |
| "The constants must be finite numbers."); |
| bool first_constant_is_larger; |
| assert(float_constant_1->type()->AsFloat()->width() == |
| float_constant_2->type()->AsFloat()->width() && |
| "First and second floating-point constants must have the same width."); |
| if (float_constant_1->type()->AsFloat()->width() == 32) { |
| first_constant_is_larger = |
| float_constant_1->GetFloat() > float_constant_2->GetFloat(); |
| } else { |
| assert(float_constant_1->type()->AsFloat()->width() == 64 && |
| "Supported floating-point widths are 32 and 64."); |
| first_constant_is_larger = |
| float_constant_1->GetDouble() > float_constant_2->GetDouble(); |
| } |
| std::vector<spv::Op> greater_than_opcodes{ |
| spv::Op::OpFOrdGreaterThan, spv::Op::OpFOrdGreaterThanEqual, |
| spv::Op::OpFUnordGreaterThan, spv::Op::OpFUnordGreaterThanEqual}; |
| std::vector<spv::Op> less_than_opcodes{ |
| spv::Op::OpFOrdGreaterThan, spv::Op::OpFOrdGreaterThanEqual, |
| spv::Op::OpFUnordGreaterThan, spv::Op::OpFUnordGreaterThanEqual}; |
| |
| ObfuscateBoolConstantViaConstantPair( |
| depth, bool_constant_use, greater_than_opcodes, less_than_opcodes, |
| float_constant_id_1, float_constant_id_2, first_constant_is_larger); |
| } |
| |
| void FuzzerPassObfuscateConstants:: |
| ObfuscateBoolConstantViaSignedIntConstantPair( |
| uint32_t depth, const protobufs::IdUseDescriptor& bool_constant_use, |
| uint32_t signed_int_constant_id_1, uint32_t signed_int_constant_id_2) { |
| auto signed_int_constant_1 = |
| GetIRContext() |
| ->get_constant_mgr() |
| ->FindDeclaredConstant(signed_int_constant_id_1) |
| ->AsIntConstant(); |
| auto signed_int_constant_2 = |
| GetIRContext() |
| ->get_constant_mgr() |
| ->FindDeclaredConstant(signed_int_constant_id_2) |
| ->AsIntConstant(); |
| assert(signed_int_constant_1->words() != signed_int_constant_2->words() && |
| "The constants should not be identical."); |
| bool first_constant_is_larger; |
| assert(signed_int_constant_1->type()->AsInteger()->width() == |
| signed_int_constant_2->type()->AsInteger()->width() && |
| "First and second floating-point constants must have the same width."); |
| assert(signed_int_constant_1->type()->AsInteger()->IsSigned()); |
| assert(signed_int_constant_2->type()->AsInteger()->IsSigned()); |
| if (signed_int_constant_1->type()->AsFloat()->width() == 32) { |
| first_constant_is_larger = |
| signed_int_constant_1->GetS32() > signed_int_constant_2->GetS32(); |
| } else { |
| assert(signed_int_constant_1->type()->AsFloat()->width() == 64 && |
| "Supported integer widths are 32 and 64."); |
| first_constant_is_larger = |
| signed_int_constant_1->GetS64() > signed_int_constant_2->GetS64(); |
| } |
| std::vector<spv::Op> greater_than_opcodes{spv::Op::OpSGreaterThan, |
| spv::Op::OpSGreaterThanEqual}; |
| std::vector<spv::Op> less_than_opcodes{spv::Op::OpSLessThan, |
| spv::Op::OpSLessThanEqual}; |
| |
| ObfuscateBoolConstantViaConstantPair( |
| depth, bool_constant_use, greater_than_opcodes, less_than_opcodes, |
| signed_int_constant_id_1, signed_int_constant_id_2, |
| first_constant_is_larger); |
| } |
| |
| void FuzzerPassObfuscateConstants:: |
| ObfuscateBoolConstantViaUnsignedIntConstantPair( |
| uint32_t depth, const protobufs::IdUseDescriptor& bool_constant_use, |
| uint32_t unsigned_int_constant_id_1, |
| uint32_t unsigned_int_constant_id_2) { |
| auto unsigned_int_constant_1 = |
| GetIRContext() |
| ->get_constant_mgr() |
| ->FindDeclaredConstant(unsigned_int_constant_id_1) |
| ->AsIntConstant(); |
| auto unsigned_int_constant_2 = |
| GetIRContext() |
| ->get_constant_mgr() |
| ->FindDeclaredConstant(unsigned_int_constant_id_2) |
| ->AsIntConstant(); |
| assert(unsigned_int_constant_1->words() != unsigned_int_constant_2->words() && |
| "The constants should not be identical."); |
| bool first_constant_is_larger; |
| assert(unsigned_int_constant_1->type()->AsInteger()->width() == |
| unsigned_int_constant_2->type()->AsInteger()->width() && |
| "First and second floating-point constants must have the same width."); |
| assert(!unsigned_int_constant_1->type()->AsInteger()->IsSigned()); |
| assert(!unsigned_int_constant_2->type()->AsInteger()->IsSigned()); |
| if (unsigned_int_constant_1->type()->AsFloat()->width() == 32) { |
| first_constant_is_larger = |
| unsigned_int_constant_1->GetU32() > unsigned_int_constant_2->GetU32(); |
| } else { |
| assert(unsigned_int_constant_1->type()->AsFloat()->width() == 64 && |
| "Supported integer widths are 32 and 64."); |
| first_constant_is_larger = |
| unsigned_int_constant_1->GetU64() > unsigned_int_constant_2->GetU64(); |
| } |
| std::vector<spv::Op> greater_than_opcodes{spv::Op::OpUGreaterThan, |
| spv::Op::OpUGreaterThanEqual}; |
| std::vector<spv::Op> less_than_opcodes{spv::Op::OpULessThan, |
| spv::Op::OpULessThanEqual}; |
| |
| ObfuscateBoolConstantViaConstantPair( |
| depth, bool_constant_use, greater_than_opcodes, less_than_opcodes, |
| unsigned_int_constant_id_1, unsigned_int_constant_id_2, |
| first_constant_is_larger); |
| } |
| |
| std::vector<std::vector<uint32_t>> |
| FuzzerPassObfuscateConstants::GetConstantWordsFromUniformsForType( |
| uint32_t type_id) { |
| assert(type_id && "Type id can't be 0"); |
| std::vector<std::vector<uint32_t>> result; |
| |
| for (const auto& facts_and_types : GetTransformationContext() |
| ->GetFactManager() |
| ->GetConstantUniformFactsAndTypes()) { |
| if (facts_and_types.second != type_id) { |
| continue; |
| } |
| |
| std::vector<uint32_t> words(facts_and_types.first.constant_word().begin(), |
| facts_and_types.first.constant_word().end()); |
| if (std::find(result.begin(), result.end(), words) == result.end()) { |
| result.push_back(std::move(words)); |
| } |
| } |
| |
| return result; |
| } |
| |
| void FuzzerPassObfuscateConstants::ObfuscateBoolConstant( |
| uint32_t depth, const protobufs::IdUseDescriptor& constant_use) { |
| // We want to replace the boolean constant use with a binary expression over |
| // scalar constants, but only if we can then potentially replace the constants |
| // with uniforms of the same value. |
| |
| auto available_types_with_uniforms = |
| GetTransformationContext() |
| ->GetFactManager() |
| ->GetTypesForWhichUniformValuesAreKnown(); |
| if (available_types_with_uniforms.empty()) { |
| // Do not try to obfuscate if we do not have access to any uniform |
| // elements with known values. |
| return; |
| } |
| auto chosen_type_id = |
| available_types_with_uniforms[GetFuzzerContext()->RandomIndex( |
| available_types_with_uniforms)]; |
| auto available_constant_words = |
| GetConstantWordsFromUniformsForType(chosen_type_id); |
| if (available_constant_words.size() == 1) { |
| // TODO(afd): for now we only obfuscate a boolean if there are at least |
| // two constants available from uniforms, so that we can do a |
| // comparison between them. It would be good to be able to do the |
| // obfuscation even if there is only one such constant, if there is |
| // also another regular constant available. |
| return; |
| } |
| |
| assert(!available_constant_words.empty() && |
| "There exists a fact but no constants - impossible"); |
| |
| // We know we have at least two known-to-be-constant uniforms of the chosen |
| // type. Pick one of them at random. |
| auto constant_index_1 = |
| GetFuzzerContext()->RandomIndex(available_constant_words); |
| uint32_t constant_index_2; |
| |
| // Now choose another one distinct from the first one. |
| do { |
| constant_index_2 = |
| GetFuzzerContext()->RandomIndex(available_constant_words); |
| } while (constant_index_1 == constant_index_2); |
| |
| auto constant_id_1 = FindOrCreateConstant( |
| available_constant_words[constant_index_1], chosen_type_id, false); |
| auto constant_id_2 = FindOrCreateConstant( |
| available_constant_words[constant_index_2], chosen_type_id, false); |
| |
| assert(constant_id_1 != 0 && constant_id_2 != 0 && |
| "We should not find an available constant with an id of 0."); |
| |
| // Now perform the obfuscation, according to whether the type of the constants |
| // is float, signed int, or unsigned int. |
| auto chosen_type = GetIRContext()->get_type_mgr()->GetType(chosen_type_id); |
| if (chosen_type->AsFloat()) { |
| ObfuscateBoolConstantViaFloatConstantPair(depth, constant_use, |
| constant_id_1, constant_id_2); |
| } else { |
| assert(chosen_type->AsInteger() && |
| "We should only have uniform facts about ints and floats."); |
| if (chosen_type->AsInteger()->IsSigned()) { |
| ObfuscateBoolConstantViaSignedIntConstantPair( |
| depth, constant_use, constant_id_1, constant_id_2); |
| } else { |
| ObfuscateBoolConstantViaUnsignedIntConstantPair( |
| depth, constant_use, constant_id_1, constant_id_2); |
| } |
| } |
| } |
| |
| void FuzzerPassObfuscateConstants::ObfuscateScalarConstant( |
| uint32_t /*depth*/, const protobufs::IdUseDescriptor& constant_use) { |
| // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/2670): consider |
| // additional ways to obfuscate scalar constants. |
| |
| // Check whether we know that any uniforms are guaranteed to be equal to the |
| // scalar constant associated with |constant_use|. |
| auto uniform_descriptors = |
| GetTransformationContext() |
| ->GetFactManager() |
| ->GetUniformDescriptorsForConstant(constant_use.id_of_interest()); |
| if (uniform_descriptors.empty()) { |
| // No relevant uniforms, so do not obfuscate. |
| return; |
| } |
| |
| // Choose a random available uniform known to be equal to the constant. |
| const auto& uniform_descriptor = |
| uniform_descriptors[GetFuzzerContext()->RandomIndex(uniform_descriptors)]; |
| |
| // Make sure the module has OpConstant instructions for each index used to |
| // access a uniform. |
| for (auto index : uniform_descriptor.index()) { |
| FindOrCreateIntegerConstant({index}, 32, true, false); |
| } |
| |
| // Make sure the module has OpTypePointer that points to the element type of |
| // the uniform. |
| const auto* uniform_variable_instr = |
| FindUniformVariable(uniform_descriptor, GetIRContext(), true); |
| assert(uniform_variable_instr && |
| "Uniform variable does not exist or not unique."); |
| |
| const auto* uniform_variable_type_intr = |
| GetIRContext()->get_def_use_mgr()->GetDef( |
| uniform_variable_instr->type_id()); |
| assert(uniform_variable_type_intr && "Uniform variable has invalid type"); |
| |
| auto element_type_id = fuzzerutil::WalkCompositeTypeIndices( |
| GetIRContext(), uniform_variable_type_intr->GetSingleWordInOperand(1), |
| uniform_descriptor.index()); |
| assert(element_type_id && "Type of uniform variable is invalid"); |
| |
| FindOrCreatePointerType(element_type_id, spv::StorageClass::Uniform); |
| |
| // Create, apply and record a transformation to replace the constant use with |
| // the result of a load from the chosen uniform. |
| ApplyTransformation(TransformationReplaceConstantWithUniform( |
| constant_use, uniform_descriptor, GetFuzzerContext()->GetFreshId(), |
| GetFuzzerContext()->GetFreshId())); |
| } |
| |
| void FuzzerPassObfuscateConstants::ObfuscateConstant( |
| uint32_t depth, const protobufs::IdUseDescriptor& constant_use) { |
| switch (GetIRContext() |
| ->get_def_use_mgr() |
| ->GetDef(constant_use.id_of_interest()) |
| ->opcode()) { |
| case spv::Op::OpConstantTrue: |
| case spv::Op::OpConstantFalse: |
| ObfuscateBoolConstant(depth, constant_use); |
| break; |
| case spv::Op::OpConstant: |
| ObfuscateScalarConstant(depth, constant_use); |
| break; |
| default: |
| assert(false && "The opcode should be one of the above."); |
| break; |
| } |
| } |
| |
| void FuzzerPassObfuscateConstants::MaybeAddConstantIdUse( |
| const opt::Instruction& inst, uint32_t in_operand_index, |
| uint32_t base_instruction_result_id, |
| const std::map<spv::Op, uint32_t>& skipped_opcode_count, |
| std::vector<protobufs::IdUseDescriptor>* constant_uses) { |
| if (inst.GetInOperand(in_operand_index).type != SPV_OPERAND_TYPE_ID) { |
| // The operand is not an id, so it cannot be a constant id. |
| return; |
| } |
| auto operand_id = inst.GetSingleWordInOperand(in_operand_index); |
| auto operand_definition = |
| GetIRContext()->get_def_use_mgr()->GetDef(operand_id); |
| switch (operand_definition->opcode()) { |
| case spv::Op::OpConstantFalse: |
| case spv::Op::OpConstantTrue: |
| case spv::Op::OpConstant: { |
| // The operand is a constant id, so make an id use descriptor and record |
| // it. |
| protobufs::IdUseDescriptor id_use_descriptor; |
| id_use_descriptor.set_id_of_interest(operand_id); |
| id_use_descriptor.mutable_enclosing_instruction() |
| ->set_target_instruction_opcode(uint32_t(inst.opcode())); |
| id_use_descriptor.mutable_enclosing_instruction() |
| ->set_base_instruction_result_id(base_instruction_result_id); |
| id_use_descriptor.mutable_enclosing_instruction() |
| ->set_num_opcodes_to_ignore( |
| skipped_opcode_count.find(inst.opcode()) == |
| skipped_opcode_count.end() |
| ? 0 |
| : skipped_opcode_count.at(inst.opcode())); |
| id_use_descriptor.set_in_operand_index(in_operand_index); |
| constant_uses->push_back(id_use_descriptor); |
| } break; |
| default: |
| break; |
| } |
| } |
| |
| void FuzzerPassObfuscateConstants::Apply() { |
| // First, gather up all the constant uses available in the module, by going |
| // through each block in each function. |
| std::vector<protobufs::IdUseDescriptor> constant_uses; |
| for (auto& function : *GetIRContext()->module()) { |
| for (auto& block : function) { |
| // For each constant use we encounter we are going to make an id use |
| // descriptor. An id use is described with respect to a base instruction; |
| // if there are instructions at the start of the block without result ids, |
| // the base instruction will have to be the block's label. |
| uint32_t base_instruction_result_id = block.id(); |
| |
| // An id use descriptor also records how many instructions of a particular |
| // opcode need to be skipped in order to find the instruction of interest |
| // from the base instruction. We maintain a mapping that records a skip |
| // count for each relevant opcode. |
| std::map<spv::Op, uint32_t> skipped_opcode_count; |
| |
| // Go through each instruction in the block. |
| for (auto& inst : block) { |
| if (inst.HasResultId()) { |
| // The instruction has a result id, so can be used as the base |
| // instruction from now on, until another instruction with a result id |
| // is encountered. |
| base_instruction_result_id = inst.result_id(); |
| // Opcode skip counts were with respect to the previous base |
| // instruction and are now irrelevant. |
| skipped_opcode_count.clear(); |
| } |
| |
| // The instruction must not be an OpVariable, the only id that an |
| // OpVariable uses is an initializer id, which has to remain |
| // constant. |
| if (inst.opcode() != spv::Op::OpVariable) { |
| // Consider each operand of the instruction, and add a constant id |
| // use for the operand if relevant. |
| for (uint32_t in_operand_index = 0; |
| in_operand_index < inst.NumInOperands(); in_operand_index++) { |
| MaybeAddConstantIdUse(inst, in_operand_index, |
| base_instruction_result_id, |
| skipped_opcode_count, &constant_uses); |
| } |
| } |
| |
| if (!inst.HasResultId()) { |
| // The instruction has no result id, so in order to identify future id |
| // uses for instructions with this opcode from the existing base |
| // instruction, we need to increase the skip count for this opcode. |
| skipped_opcode_count[inst.opcode()] = |
| skipped_opcode_count.find(inst.opcode()) == |
| skipped_opcode_count.end() |
| ? 1 |
| : skipped_opcode_count[inst.opcode()] + 1; |
| } |
| } |
| } |
| } |
| |
| // Go through the constant uses in a random order by repeatedly pulling out a |
| // constant use at a random index. |
| while (!constant_uses.empty()) { |
| auto index = GetFuzzerContext()->RandomIndex(constant_uses); |
| auto constant_use = std::move(constant_uses[index]); |
| constant_uses.erase(constant_uses.begin() + index); |
| // Decide probabilistically whether to skip or obfuscate this constant use. |
| if (!GetFuzzerContext()->ChoosePercentage( |
| GetFuzzerContext()->GetChanceOfObfuscatingConstant())) { |
| continue; |
| } |
| ObfuscateConstant(0, constant_use); |
| } |
| } |
| |
| } // namespace fuzz |
| } // namespace spvtools |