spirv-fuzz: Make functions "livesafe" during donation (#3146)

This change allows the generator to (optionally and at random) make
the functions of a module "livesafe" during donation. This involves
introducing a loop limiter variable to each function and gating the
number of total loop iterations for the function using that variable.
It also involves eliminating OpKill and OpUnreachable instructions
(changing them to OpReturn/OpReturnValue), and clamping access chain
indices so that they are always in-bounds.
This commit is contained in:
Alastair Donaldson 2020-01-29 15:52:31 +00:00 коммит произвёл GitHub
Родитель 97f1d485b7
Коммит 521223b70a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 3629 добавлений и 44 удалений

Просмотреть файл

@ -801,7 +801,7 @@ bool FactManager::DataSynonymFacts::IsSynonymous(
//==============================
//==============================
// Dead id facts
// Dead block facts
// The purpose of this class is to group the fields and data used to represent
// facts about data blocks.
@ -829,10 +829,41 @@ bool FactManager::DeadBlockFacts::BlockIsDead(uint32_t block_id) const {
// End of dead block facts
//==============================
//==============================
// Livesafe function facts
// The purpose of this class is to group the fields and data used to represent
// facts about livesafe functions.
class FactManager::LivesafeFunctionFacts {
public:
// See method in FactManager which delegates to this method.
void AddFact(const protobufs::FactFunctionIsLivesafe& fact);
// See method in FactManager which delegates to this method.
bool FunctionIsLivesafe(uint32_t function_id) const;
private:
std::set<uint32_t> livesafe_function_ids_;
};
void FactManager::LivesafeFunctionFacts::AddFact(
const protobufs::FactFunctionIsLivesafe& fact) {
livesafe_function_ids_.insert(fact.function_id());
}
bool FactManager::LivesafeFunctionFacts::FunctionIsLivesafe(
uint32_t function_id) const {
return livesafe_function_ids_.count(function_id) != 0;
}
// End of livesafe function facts
//==============================
FactManager::FactManager()
: uniform_constant_facts_(MakeUnique<ConstantUniformFacts>()),
data_synonym_facts_(MakeUnique<DataSynonymFacts>()),
dead_block_facts_(MakeUnique<DeadBlockFacts>()) {}
dead_block_facts_(MakeUnique<DeadBlockFacts>()),
livesafe_function_facts_(MakeUnique<LivesafeFunctionFacts>()) {}
FactManager::~FactManager() = default;
@ -860,6 +891,9 @@ bool FactManager::AddFact(const fuzz::protobufs::Fact& fact,
case protobufs::Fact::kBlockIsDeadFact:
dead_block_facts_->AddFact(fact.block_is_dead_fact());
return true;
case protobufs::Fact::kFunctionIsLivesafeFact:
livesafe_function_facts_->AddFact(fact.function_is_livesafe_fact());
return true;
default:
assert(false && "Unknown fact type.");
return false;
@ -941,5 +975,15 @@ void FactManager::AddFactBlockIsDead(uint32_t block_id) {
dead_block_facts_->AddFact(fact);
}
bool FactManager::FunctionIsLivesafe(uint32_t function_id) const {
return livesafe_function_facts_->FunctionIsLivesafe(function_id);
}
void FactManager::AddFactFunctionIsLivesafe(uint32_t function_id) {
protobufs::FactFunctionIsLivesafe fact;
fact.set_function_id(function_id);
livesafe_function_facts_->AddFact(fact);
}
} // namespace fuzz
} // namespace spvtools

Просмотреть файл

@ -61,6 +61,9 @@ class FactManager {
// Records the fact that |block_id| is dead.
void AddFactBlockIsDead(uint32_t block_id);
// Records the fact that |function_id| is livesafe.
void AddFactFunctionIsLivesafe(uint32_t function_id);
// The fact manager is responsible for managing a few distinct categories of
// facts. In principle there could be different fact managers for each kind
// of fact, but in practice providing one 'go to' place for facts is
@ -143,6 +146,16 @@ class FactManager {
// End of dead block facts
//==============================
//==============================
// Querying facts about livesafe function
// Returns true if and ony if |function_id| is the id of a function known
// to be livesafe.
bool FunctionIsLivesafe(uint32_t function_id) const;
// End of dead block facts
//==============================
private:
// For each distinct kind of fact to be managed, we use a separate opaque
// class type.
@ -159,6 +172,11 @@ class FactManager {
class DeadBlockFacts; // Opaque class for management of dead block facts.
std::unique_ptr<DeadBlockFacts>
dead_block_facts_; // Unique pointer to internal data.
class LivesafeFunctionFacts; // Opaque class for management of livesafe
// function facts.
std::unique_ptr<LivesafeFunctionFacts>
livesafe_function_facts_; // Unique pointer to internal data.
};
} // namespace fuzz

Просмотреть файл

@ -38,6 +38,7 @@ const std::pair<uint32_t, uint32_t> kChanceOfAdjustingSelectionControl = {20,
const std::pair<uint32_t, uint32_t> kChanceOfConstructingComposite = {20, 50};
const std::pair<uint32_t, uint32_t> kChanceOfCopyingObject = {20, 50};
const std::pair<uint32_t, uint32_t> kChanceOfDonatingAdditionalModule = {5, 50};
const std::pair<uint32_t, uint32_t> kChanceOfMakingDonorLivesafe = {40, 60};
const std::pair<uint32_t, uint32_t> kChanceOfMergingBlocks = {20, 95};
const std::pair<uint32_t, uint32_t> kChanceOfMovingBlockDown = {20, 50};
const std::pair<uint32_t, uint32_t> kChanceOfObfuscatingConstant = {10, 90};
@ -49,6 +50,7 @@ const std::pair<uint32_t, uint32_t> kChanceOfSplittingBlock = {40, 95};
// Keep them in alphabetical order.
const uint32_t kDefaultMaxLoopControlPartialCount = 100;
const uint32_t kDefaultMaxLoopControlPeelCount = 100;
const uint32_t kDefaultMaxLoopLimit = 20;
// Default functions for controlling how deep to go during recursive
// generation/transformation. Keep them in alphabetical order.
@ -89,6 +91,8 @@ FuzzerContext::FuzzerContext(RandomGenerator* random_generator,
chance_of_copying_object_ = ChooseBetweenMinAndMax(kChanceOfCopyingObject);
chance_of_donating_additional_module_ =
ChooseBetweenMinAndMax(kChanceOfDonatingAdditionalModule);
chance_of_making_donor_livesafe_ =
ChooseBetweenMinAndMax(kChanceOfMakingDonorLivesafe);
chance_of_merging_blocks_ = ChooseBetweenMinAndMax(kChanceOfMergingBlocks);
chance_of_moving_block_down_ =
ChooseBetweenMinAndMax(kChanceOfMovingBlockDown);
@ -101,6 +105,7 @@ FuzzerContext::FuzzerContext(RandomGenerator* random_generator,
chance_of_splitting_block_ = ChooseBetweenMinAndMax(kChanceOfSplittingBlock);
max_loop_control_partial_count_ = kDefaultMaxLoopControlPartialCount;
max_loop_control_peel_count_ = kDefaultMaxLoopControlPeelCount;
max_loop_limit_ = kDefaultMaxLoopLimit;
}
FuzzerContext::~FuzzerContext() = default;

Просмотреть файл

@ -85,6 +85,9 @@ class FuzzerContext {
uint32_t GetChanceOfDonatingAdditionalModule() {
return chance_of_donating_additional_module_;
}
uint32_t ChanceOfMakingDonorLivesafe() {
return chance_of_making_donor_livesafe_;
}
uint32_t GetChanceOfMergingBlocks() { return chance_of_merging_blocks_; }
uint32_t GetChanceOfMovingBlockDown() { return chance_of_moving_block_down_; }
uint32_t GetChanceOfObfuscatingConstant() {
@ -103,6 +106,9 @@ class FuzzerContext {
uint32_t GetRandomLoopControlPartialCount() {
return random_generator_->RandomUint32(max_loop_control_partial_count_);
}
uint32_t GetRandomLoopLimit() {
return random_generator_->RandomUint32(max_loop_limit_);
}
// Functions to control how deeply to recurse.
// Keep them in alphabetical order.
@ -129,6 +135,7 @@ class FuzzerContext {
uint32_t chance_of_constructing_composite_;
uint32_t chance_of_copying_object_;
uint32_t chance_of_donating_additional_module_;
uint32_t chance_of_making_donor_livesafe_;
uint32_t chance_of_merging_blocks_;
uint32_t chance_of_moving_block_down_;
uint32_t chance_of_obfuscating_constant_;
@ -141,6 +148,7 @@ class FuzzerContext {
// Keep them in alphabetical order.
uint32_t max_loop_control_partial_count_;
uint32_t max_loop_control_peel_count_;
uint32_t max_loop_limit_;
// Functions to determine with what probability to go deeper when generating
// or mutating constructs recursively.

Просмотреть файл

@ -15,6 +15,11 @@
#include "source/fuzz/fuzzer_pass.h"
#include "source/fuzz/instruction_descriptor.h"
#include "source/fuzz/transformation_add_constant_scalar.h"
#include "source/fuzz/transformation_add_global_undef.h"
#include "source/fuzz/transformation_add_type_boolean.h"
#include "source/fuzz/transformation_add_type_int.h"
#include "source/fuzz/transformation_add_type_pointer.h"
namespace spvtools {
namespace fuzz {
@ -128,5 +133,73 @@ void FuzzerPass::MaybeAddTransformationBeforeEachInstruction(
}
}
uint32_t FuzzerPass::FindOrCreateBoolType() {
opt::analysis::Bool bool_type;
auto existing_id = GetIRContext()->get_type_mgr()->GetId(&bool_type);
if (existing_id) {
return existing_id;
}
auto result = GetFuzzerContext()->GetFreshId();
ApplyTransformation(TransformationAddTypeBoolean(result));
return result;
}
uint32_t FuzzerPass::FindOrCreate32BitIntegerType(bool is_signed) {
opt::analysis::Integer int_type(32, is_signed);
auto existing_id = GetIRContext()->get_type_mgr()->GetId(&int_type);
if (existing_id) {
return existing_id;
}
auto result = GetFuzzerContext()->GetFreshId();
ApplyTransformation(TransformationAddTypeInt(result, 32, is_signed));
return result;
}
uint32_t FuzzerPass::FindOrCreatePointerTo32BitIntegerType(
bool is_signed, SpvStorageClass storage_class) {
auto uint32_type_id = FindOrCreate32BitIntegerType(is_signed);
opt::analysis::Pointer pointer_type(
GetIRContext()->get_type_mgr()->GetType(uint32_type_id), storage_class);
auto existing_id = GetIRContext()->get_type_mgr()->GetId(&pointer_type);
if (existing_id) {
return existing_id;
}
auto result = GetFuzzerContext()->GetFreshId();
ApplyTransformation(
TransformationAddTypePointer(result, storage_class, uint32_type_id));
return result;
}
uint32_t FuzzerPass::FindOrCreate32BitIntegerConstant(uint32_t word,
bool is_signed) {
auto uint32_type_id = FindOrCreate32BitIntegerType(is_signed);
opt::analysis::IntConstant int_constant(
GetIRContext()->get_type_mgr()->GetType(uint32_type_id)->AsInteger(),
{word});
auto existing_constant =
GetIRContext()->get_constant_mgr()->FindConstant(&int_constant);
if (existing_constant) {
return GetIRContext()
->get_constant_mgr()
->GetDefiningInstruction(existing_constant)
->result_id();
}
auto result = GetFuzzerContext()->GetFreshId();
ApplyTransformation(
TransformationAddConstantScalar(result, uint32_type_id, {word}));
return result;
}
uint32_t FuzzerPass::FindOrCreateGlobalUndef(uint32_t type_id) {
for (auto& inst : GetIRContext()->types_values()) {
if (inst.opcode() == SpvOpUndef && inst.type_id() == type_id) {
return inst.result_id();
}
}
auto result = GetFuzzerContext()->GetFreshId();
ApplyTransformation(TransformationAddGlobalUndef(result, type_id));
return result;
}
} // namespace fuzz
} // namespace spvtools

Просмотреть файл

@ -89,7 +89,7 @@ class FuzzerPass {
const protobufs::InstructionDescriptor& instruction_descriptor)>
maybe_apply_transformation);
// A generic helper for applying a transforamtion that should be appplicable
// A generic helper for applying a transformation that should be applicable
// by construction, and adding it to the sequence of applied transformations.
template <typename TransformationType>
void ApplyTransformation(const TransformationType& transformation) {
@ -99,6 +99,33 @@ class FuzzerPass {
*GetTransformations()->add_transformation() = transformation.ToMessage();
}
// Returns the id of an OpTypeBool instruction. If such an instruction does
// not exist, a transformation is applied to add it.
uint32_t FindOrCreateBoolType();
// Returns the id of an OpTypeInt instruction, with width 32 and signedness
// specified by |is_signed|. If such an instruction does not exist, a
// transformation is applied to add it.
uint32_t FindOrCreate32BitIntegerType(bool is_signed);
// Returns the id of an OpTypePointer instruction, with a 32-bit integer base
// type of signedness specified by |is_signed|. If the pointer type or
// required integer base type do not exist, transformations are applied to add
// them.
uint32_t FindOrCreatePointerTo32BitIntegerType(bool is_signed,
SpvStorageClass storage_class);
// Returns the id of an OpConstant instruction, with 32-bit integer type of
// signedness specified by |is_signed|, with |word| as its value. If either
// the required integer type or the constant do not exist, transformations are
// applied to add them.
uint32_t FindOrCreate32BitIntegerConstant(uint32_t word, bool is_signed);
// Returns the result id of an instruction of the form:
// %id = OpUndef %|type_id|
// If no such instruction exists, a transformation is applied to add it.
uint32_t FindOrCreateGlobalUndef(uint32_t type_id);
private:
opt::IRContext* ir_context_;
FactManager* fact_manager_;

Просмотреть файл

@ -62,13 +62,20 @@ void FuzzerPassDonateModules::Apply() {
GetFuzzerContext()->RandomIndex(donor_suppliers_))();
assert(donor_ir_context != nullptr && "Supplying of donor failed");
// Donate the supplied module.
DonateSingleModule(donor_ir_context.get());
//
// Randomly decide whether to make the module livesafe (see
// FactFunctionIsLivesafe); doing so allows it to be used for live code
// injection but restricts its behaviour to allow this, and means that its
// functions cannot be transformed as if they were arbitrary dead code.
bool make_livesafe = GetFuzzerContext()->ChoosePercentage(
GetFuzzerContext()->ChanceOfMakingDonorLivesafe());
DonateSingleModule(donor_ir_context.get(), make_livesafe);
} while (GetFuzzerContext()->ChoosePercentage(
GetFuzzerContext()->GetChanceOfDonatingAdditionalModule()));
}
void FuzzerPassDonateModules::DonateSingleModule(
opt::IRContext* donor_ir_context) {
opt::IRContext* donor_ir_context, bool make_livesafe) {
// The ids used by the donor module may very well clash with ids defined in
// the recipient module. Furthermore, some instructions defined in the donor
// module will be equivalent to instructions defined in the recipient module,
@ -91,7 +98,7 @@ void FuzzerPassDonateModules::DonateSingleModule(
HandleExternalInstructionImports(donor_ir_context,
&original_id_to_donated_id);
HandleTypesAndValues(donor_ir_context, &original_id_to_donated_id);
HandleFunctions(donor_ir_context, &original_id_to_donated_id);
HandleFunctions(donor_ir_context, &original_id_to_donated_id, make_livesafe);
// TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3115) Handle some
// kinds of decoration.
@ -420,7 +427,8 @@ void FuzzerPassDonateModules::HandleTypesAndValues(
void FuzzerPassDonateModules::HandleFunctions(
opt::IRContext* donor_ir_context,
std::map<uint32_t, uint32_t>* original_id_to_donated_id) {
std::map<uint32_t, uint32_t>* original_id_to_donated_id,
bool make_livesafe) {
// Get the ids of functions in the donor module, topologically sorted
// according to the donor's call graph.
auto topological_order =
@ -506,7 +514,146 @@ void FuzzerPassDonateModules::HandleFunctions(
: 0,
input_operands));
});
ApplyTransformation(TransformationAddFunction(donated_instructions));
if (make_livesafe) {
// Various types and constants must be in place for a function to be made
// live-safe. Add them if not already present.
FindOrCreateBoolType(); // Needed for comparisons
FindOrCreatePointerTo32BitIntegerType(
false, SpvStorageClassFunction); // Needed for adding loop limiters
FindOrCreate32BitIntegerConstant(
0, false); // Needed for initializing loop limiters
FindOrCreate32BitIntegerConstant(
1, false); // Needed for incrementing loop limiters
// Get a fresh id for the variable that will be used as a loop limiter.
const uint32_t loop_limiter_variable_id =
GetFuzzerContext()->GetFreshId();
// Choose a random loop limit, and add the required constant to the
// module if not already there.
const uint32_t loop_limit = FindOrCreate32BitIntegerConstant(
GetFuzzerContext()->GetRandomLoopLimit(), false);
// Consider every loop header in the function to donate, and create a
// structure capturing the ids to be used for manipulating the loop
// limiter each time the loop is iterated.
std::vector<protobufs::LoopLimiterInfo> loop_limiters;
for (auto& block : *function_to_donate) {
if (block.IsLoopHeader()) {
protobufs::LoopLimiterInfo loop_limiter;
// Grab the loop header's id, mapped to its donated value.
loop_limiter.set_loop_header_id(
original_id_to_donated_id->at(block.id()));
// Get fresh ids that will be used to load the loop limiter, increment
// it, compare it with the loop limit, and an id for a new block that
// will contain the loop's original terminator.
loop_limiter.set_load_id(GetFuzzerContext()->GetFreshId());
loop_limiter.set_increment_id(GetFuzzerContext()->GetFreshId());
loop_limiter.set_compare_id(GetFuzzerContext()->GetFreshId());
loop_limiter.set_logical_op_id(GetFuzzerContext()->GetFreshId());
loop_limiters.emplace_back(loop_limiter);
}
}
// Consider every access chain in the function to donate, and create a
// structure containing the ids necessary to clamp the access chain
// indices to be in-bounds.
std::vector<protobufs::AccessChainClampingInfo>
access_chain_clamping_info;
for (auto& block : *function_to_donate) {
for (auto& inst : block) {
switch (inst.opcode()) {
case SpvOpAccessChain:
case SpvOpInBoundsAccessChain: {
protobufs::AccessChainClampingInfo clamping_info;
clamping_info.set_access_chain_id(
original_id_to_donated_id->at(inst.result_id()));
auto base_object = donor_ir_context->get_def_use_mgr()->GetDef(
inst.GetSingleWordInOperand(0));
assert(base_object && "The base object must exist.");
auto pointer_type = donor_ir_context->get_def_use_mgr()->GetDef(
base_object->type_id());
assert(pointer_type &&
pointer_type->opcode() == SpvOpTypePointer &&
"The base object must have pointer type.");
auto should_be_composite_type =
donor_ir_context->get_def_use_mgr()->GetDef(
pointer_type->GetSingleWordInOperand(1));
// Walk the access chain, creating fresh ids to facilitate
// clamping each index. For simplicity we do this for every
// index, even though constant indices will not end up being
// clamped.
for (uint32_t index = 1; index < inst.NumInOperands(); index++) {
auto compare_and_select_ids =
clamping_info.add_compare_and_select_ids();
compare_and_select_ids->set_first(
GetFuzzerContext()->GetFreshId());
compare_and_select_ids->set_second(
GetFuzzerContext()->GetFreshId());
// Get the bound for the component being indexed into.
uint32_t bound =
TransformationAddFunction::GetBoundForCompositeIndex(
donor_ir_context, *should_be_composite_type);
const uint32_t index_id = inst.GetSingleWordInOperand(index);
auto index_inst =
donor_ir_context->get_def_use_mgr()->GetDef(index_id);
auto index_type_inst =
donor_ir_context->get_def_use_mgr()->GetDef(
index_inst->type_id());
assert(index_type_inst->opcode() == SpvOpTypeInt);
assert(index_type_inst->GetSingleWordInOperand(0) == 32);
opt::analysis::Integer* index_int_type =
donor_ir_context->get_type_mgr()
->GetType(index_type_inst->result_id())
->AsInteger();
if (index_inst->opcode() != SpvOpConstant) {
// We will have to clamp this index, so we need a constant
// whose value is one less than the bound, to compare
// against and to use as the clamped value.
FindOrCreate32BitIntegerConstant(bound - 1,
index_int_type->IsSigned());
}
should_be_composite_type =
TransformationAddFunction::FollowCompositeIndex(
donor_ir_context, *should_be_composite_type, index_id);
}
access_chain_clamping_info.push_back(clamping_info);
break;
}
default:
break;
}
}
}
// If the function contains OpKill or OpUnreachable instructions, and has
// non-void return type, then we need a value %v to use in order to turn
// these into instructions of the form OpReturn %v.
uint32_t kill_unreachable_return_value_id;
auto function_return_type_inst =
donor_ir_context->get_def_use_mgr()->GetDef(
function_to_donate->type_id());
if (function_return_type_inst->opcode() == SpvOpTypeVoid) {
// The return type is void, so we don't need a return value.
kill_unreachable_return_value_id = 0;
} else {
// We do need a return value; we use OpUndef.
kill_unreachable_return_value_id =
FindOrCreateGlobalUndef(function_return_type_inst->type_id());
}
// Add the function in a livesafe manner.
ApplyTransformation(TransformationAddFunction(
donated_instructions, loop_limiter_variable_id, loop_limit,
loop_limiters, kill_unreachable_return_value_id,
access_chain_clamping_info));
} else {
// Add the function in a non-livesafe manner.
ApplyTransformation(TransformationAddFunction(donated_instructions));
}
}
}

Просмотреть файл

@ -38,8 +38,10 @@ class FuzzerPassDonateModules : public FuzzerPass {
void Apply() override;
// Donates the global declarations and functions of |donor_ir_context| into
// the fuzzer pass's IR context.
void DonateSingleModule(opt::IRContext* donor_ir_context);
// the fuzzer pass's IR context. |make_livesafe| dictates whether the
// functions of the donated module will be made livesafe (see
// FactFunctionIsLivesafe).
void DonateSingleModule(opt::IRContext* donor_ir_context, bool make_livesafe);
private:
// Adapts a storage class coming from a donor module so that it will work
@ -68,9 +70,12 @@ class FuzzerPassDonateModules : public FuzzerPass {
// functions in |donor_ir_context|'s call graph in a reverse-topologically-
// sorted order (leaves-to-root), adding each function to the recipient
// module, rewritten to use fresh ids and using |original_id_to_donated_id| to
// remap ids.
// remap ids. The |make_livesafe| argument captures whether the functions in
// the module are required to be made livesafe before being added to the
// recipient.
void HandleFunctions(opt::IRContext* donor_ir_context,
std::map<uint32_t, uint32_t>* original_id_to_donated_id);
std::map<uint32_t, uint32_t>* original_id_to_donated_id,
bool make_livesafe);
// Returns the ids of all functions in |context| in a topological order in
// relation to the call graph of |context|, which is assumed to be recursion-

Просмотреть файл

@ -103,10 +103,10 @@ bool PhiIdsOkForNewEdge(
}
phi_index++;
}
// Return false if not all of the ids for extending OpPhi instructions are
// needed. This might turn out to be stricter than necessary; perhaps it would
// be OK just to not use the ids in this case.
return phi_index == static_cast<uint32_t>(phi_ids.size());
// We allow some of the ids provided for extending OpPhi instructions to be
// unused. Their presence does no harm, and requiring a perfect match may
// make transformations less likely to cleanly apply.
return true;
}
uint32_t MaybeGetBoolConstantId(opt::IRContext* context, bool value) {
@ -158,13 +158,11 @@ void AddUnreachableEdgeAndUpdateOpPhis(
break;
}
assert(phi_index < static_cast<uint32_t>(phi_ids.size()) &&
"There should be exactly one phi id per OpPhi instruction.");
"There should be at least one phi id per OpPhi instruction.");
inst.AddOperand({SPV_OPERAND_TYPE_ID, {phi_ids[phi_index]}});
inst.AddOperand({SPV_OPERAND_TYPE_ID, {bb_from->id()}});
phi_index++;
}
assert(phi_index == static_cast<uint32_t>(phi_ids.size()) &&
"There should be exactly one phi id per OpPhi instruction.");
}
}

Просмотреть файл

@ -167,6 +167,7 @@ message Fact {
FactConstantUniform constant_uniform_fact = 1;
FactDataSynonym data_synonym_fact = 2;
FactBlockIsDead block_is_dead_fact = 3;
FactFunctionIsLivesafe function_is_livesafe_fact = 4;
}
}
@ -210,6 +211,77 @@ message FactBlockIsDead {
uint32 block_id = 1;
}
message FactFunctionIsLivesafe {
// Records the fact that a function is guaranteed to be "livesafe", meaning
// that it will not make out-of-bounds accesses, does not contain reachable
// OpKill or OpUnreachable instructions, does not contain loops that will
// execute for large numbers of iterations, and only invokes other livesafe
// functions.
uint32 function_id = 1;
}
message AccessChainClampingInfo {
// When making a function livesafe it is necessary to clamp the indices that
// occur as operands to access chain instructions so that they are guaranteed
// to be in bounds. This message type allows an access chain instruction to
// have an associated sequence of ids that are reserved for comparing an
// access chain index with a bound (e.g. an array size), and selecting
// between the access chain index (if it is within bounds) and the bound (if
// it is not).
//
// This allows turning an instruction of the form:
//
// %result = OpAccessChain %type %object ... %index ...
//
// into:
//
// %t1 = OpULessThanEqual %bool %index %bound_minus_one
// %t2 = OpSelect %int_type %t1 %index %bound_minus_one
// %result = OpAccessChain %type %object ... %t2 ...
// The result id of an OpAccessChain or OpInBoundsAccessChain instruction.
uint32 access_chain_id = 1;
// A series of pairs of fresh ids, one per access chain index, for the results
// of a compare instruction and a select instruction, serving the roles of %t1
// and %t2 in the above example.
repeated UInt32Pair compare_and_select_ids = 2;
}
message LoopLimiterInfo {
// Structure capturing the information required to manipulate a loop limiter
// at a loop header.
// The header for the loop.
uint32 loop_header_id = 1;
// A fresh id into which the loop limiter's current value can be loaded.
uint32 load_id = 2;
// A fresh id that can be used to increment the loaded value by 1.
uint32 increment_id = 3;
// A fresh id that can be used to compare the loaded value with the loop
// limit.
uint32 compare_id = 4;
// A fresh id that can be used to compute the conjunction or disjunction of
// an original loop exit condition with |compare_id|, if the loop's back edge
// block can conditionally exit the loop.
uint32 logical_op_id = 5;
// A sequence of ids suitable for extending OpPhi instructions of the loop
// merge block if it did not previously have an incoming edge from the loop
// back edge block.
repeated uint32 phi_id = 6;
}
message TransformationSequence {
repeated Transformation transformation = 1;
}
@ -366,6 +438,33 @@ message TransformationAddFunction {
// The series of instructions that comprise the function.
repeated Instruction instruction = 1;
// True if and only if the given function should be made livesafe (see
// FactFunctionIsLivesafe for definition).
bool is_livesafe = 2;
// Fresh id for a new variable that will serve as a "loop limiter" for the
// function; only relevant if |is_livesafe| holds.
uint32 loop_limiter_variable_id = 3;
// Id of an existing unsigned integer constant providing the maximum value
// that the loop limiter can reach before the loop is broken from; only
// relevant if |is_livesafe| holds.
uint32 loop_limit_constant_id = 4;
// Fresh ids for each loop in the function that allow the loop limiter to be
// manipulated; only relevant if |is_livesafe| holds.
repeated LoopLimiterInfo loop_limiter_info = 5;
// Id of an existing global value with the same return type as the function
// that can be used to replace OpKill and OpReachable instructions with
// ReturnValue instructions. Ignored if the function has void return type.
uint32 kill_unreachable_return_value_id = 6;
// A mapping (represented as a sequence) from every access chain result id in
// the function to the ids required to clamp its indices to ensure they are in
// bounds.
repeated AccessChainClampingInfo access_chain_clamping_info = 7;
}
message TransformationAddGlobalUndef {

Просмотреть файл

@ -29,11 +29,95 @@ TransformationAddFunction::TransformationAddFunction(
for (auto& instruction : instructions) {
*message_.add_instruction() = instruction;
}
message_.set_is_livesafe(false);
}
TransformationAddFunction::TransformationAddFunction(
const std::vector<protobufs::Instruction>& instructions,
uint32_t loop_limiter_variable_id, uint32_t loop_limit_constant_id,
const std::vector<protobufs::LoopLimiterInfo>& loop_limiters,
uint32_t kill_unreachable_return_value_id,
const std::vector<protobufs::AccessChainClampingInfo>&
access_chain_clampers) {
for (auto& instruction : instructions) {
*message_.add_instruction() = instruction;
}
message_.set_is_livesafe(true);
message_.set_loop_limiter_variable_id(loop_limiter_variable_id);
message_.set_loop_limit_constant_id(loop_limit_constant_id);
for (auto& loop_limiter : loop_limiters) {
*message_.add_loop_limiter_info() = loop_limiter;
}
message_.set_kill_unreachable_return_value_id(
kill_unreachable_return_value_id);
for (auto& access_clamper : access_chain_clampers) {
*message_.add_access_chain_clamping_info() = access_clamper;
}
}
bool TransformationAddFunction::IsApplicable(
opt::IRContext* context,
const spvtools::fuzz::FactManager& /*unused*/) const {
const spvtools::fuzz::FactManager& fact_manager) const {
// This transformation may use a lot of ids, all of which need to be fresh
// and distinct. This set tracks them.
std::set<uint32_t> ids_used_by_this_transformation;
// Ensure that all result ids in the new function are fresh and distinct.
for (auto& instruction : message_.instruction()) {
if (instruction.result_id()) {
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
instruction.result_id(), context,
&ids_used_by_this_transformation)) {
return false;
}
}
}
if (message_.is_livesafe()) {
// Ensure that all ids provided for making the function livesafe are fresh
// and distinct.
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
message_.loop_limiter_variable_id(), context,
&ids_used_by_this_transformation)) {
return false;
}
for (auto& loop_limiter_info : message_.loop_limiter_info()) {
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
loop_limiter_info.load_id(), context,
&ids_used_by_this_transformation)) {
return false;
}
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
loop_limiter_info.increment_id(), context,
&ids_used_by_this_transformation)) {
return false;
}
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
loop_limiter_info.compare_id(), context,
&ids_used_by_this_transformation)) {
return false;
}
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
loop_limiter_info.logical_op_id(), context,
&ids_used_by_this_transformation)) {
return false;
}
}
for (auto& access_chain_clamping_info :
message_.access_chain_clamping_info()) {
for (auto& pair : access_chain_clamping_info.compare_and_select_ids()) {
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
pair.first(), context, &ids_used_by_this_transformation)) {
return false;
}
if (!CheckIdIsFreshAndNotUsedByThisTransformation(
pair.second(), context, &ids_used_by_this_transformation)) {
return false;
}
}
}
}
// Because checking all the conditions for a function to be valid is a big
// job that the SPIR-V validator can already do, a "try it and see" approach
// is taken here.
@ -47,18 +131,49 @@ bool TransformationAddFunction::IsApplicable(
if (!TryToAddFunction(cloned_module.get())) {
return false;
}
// Having managed to add the new function to the cloned module, we ascertain
// whether the cloned module is still valid. If it is, the transformation is
// applicable.
if (message_.is_livesafe()) {
// We make the cloned module livesafe.
if (!TryToMakeFunctionLivesafe(cloned_module.get(), fact_manager)) {
return false;
}
}
// Having managed to add the new function to the cloned module, and
// potentially also made it livesafe, we ascertain whether the cloned module
// is still valid. If it is, the transformation is applicable.
return fuzzerutil::IsValid(cloned_module.get());
}
void TransformationAddFunction::Apply(
opt::IRContext* context, spvtools::fuzz::FactManager* /*unused*/) const {
auto success = TryToAddFunction(context);
opt::IRContext* context, spvtools::fuzz::FactManager* fact_manager) const {
// Add the function to the module. As the transformation is applicable, this
// should succeed.
bool success = TryToAddFunction(context);
assert(success && "The function should be successfully added.");
(void)(success); // Keep release builds happy (otherwise they may complain
// that |success| is not used).
if (message_.is_livesafe()) {
// Make the function livesafe, which also should succeed.
success = TryToMakeFunctionLivesafe(context, *fact_manager);
assert(success && "It should be possible to make the function livesafe.");
(void)(success); // Keep release builds happy.
// Inform the fact manager that the function is livesafe.
assert(message_.instruction(0).opcode() == SpvOpFunction &&
"The first instruction of an 'add function' transformation must be "
"OpFunction.");
fact_manager->AddFactFunctionIsLivesafe(
message_.instruction(0).result_id());
} else {
// Inform the fact manager that all blocks in the function are dead.
for (auto& inst : message_.instruction()) {
if (inst.opcode() == SpvOpLabel) {
fact_manager->AddFactBlockIsDead(inst.result_id());
}
}
}
context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
}
@ -149,8 +264,634 @@ bool TransformationAddFunction::TryToAddFunction(
new_function->SetFunctionEnd(
InstructionFromMessage(context, message_.instruction(instruction_index)));
context->AddFunction(std::move(new_function));
context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
return true;
}
bool TransformationAddFunction::TryToMakeFunctionLivesafe(
opt::IRContext* context, const FactManager& fact_manager) const {
assert(message_.is_livesafe() && "Precondition: is_livesafe must hold.");
// Get a pointer to the added function.
opt::Function* added_function = nullptr;
for (auto& function : *context->module()) {
if (function.result_id() == message_.instruction(0).result_id()) {
added_function = &function;
break;
}
}
assert(added_function && "The added function should have been found.");
if (!TryToAddLoopLimiters(context, added_function)) {
// Adding loop limiters did not work; bail out.
return false;
}
// Consider all the instructions in the function, and:
// - attempt to replace OpKill and OpUnreachable with return instructions
// - attempt to clamp access chains to be within bounds
// - check that OpFunctionCall instructions are only to livesafe functions
for (auto& block : *added_function) {
for (auto& inst : block) {
switch (inst.opcode()) {
case SpvOpKill:
case SpvOpUnreachable:
if (!TryToTurnKillOrUnreachableIntoReturn(context, added_function,
&inst)) {
return false;
}
break;
case SpvOpAccessChain:
case SpvOpInBoundsAccessChain:
if (!TryToClampAccessChainIndices(context, &inst)) {
return false;
}
break;
case SpvOpFunctionCall:
// A livesafe function my only call other livesafe functions.
if (!fact_manager.FunctionIsLivesafe(
inst.GetSingleWordInOperand(0))) {
return false;
}
default:
break;
}
}
}
return true;
}
bool TransformationAddFunction::TryToAddLoopLimiters(
opt::IRContext* context, opt::Function* added_function) const {
// Collect up all the loop headers so that we can subsequently add loop
// limiting logic.
std::vector<opt::BasicBlock*> loop_headers;
for (auto& block : *added_function) {
if (block.IsLoopHeader()) {
loop_headers.push_back(&block);
}
}
if (loop_headers.empty()) {
// There are no loops, so no need to add any loop limiters.
return true;
}
// Check that the module contains appropriate ingredients for declaring and
// manipulating a loop limiter.
auto loop_limit_constant_id_instr =
context->get_def_use_mgr()->GetDef(message_.loop_limit_constant_id());
if (!loop_limit_constant_id_instr ||
loop_limit_constant_id_instr->opcode() != SpvOpConstant) {
// The loop limit constant id instruction must exist and have an
// appropriate opcode.
return false;
}
auto loop_limit_type = context->get_def_use_mgr()->GetDef(
loop_limit_constant_id_instr->type_id());
if (loop_limit_type->opcode() != SpvOpTypeInt ||
loop_limit_type->GetSingleWordInOperand(0) != 32) {
// The type of the loop limit constant must be 32-bit integer. It
// doesn't actually matter whether the integer is signed or not.
return false;
}
// Find the id of the "unsigned int" type.
opt::analysis::Integer unsigned_int_type(32, false);
uint32_t unsigned_int_type_id =
context->get_type_mgr()->GetId(&unsigned_int_type);
if (!unsigned_int_type_id) {
// Unsigned int is not available; we need this type in order to add loop
// limiters.
return false;
}
auto registered_unsigned_int_type =
context->get_type_mgr()->GetRegisteredType(&unsigned_int_type);
// Look for 0 of type unsigned int.
opt::analysis::IntConstant zero(registered_unsigned_int_type->AsInteger(),
{0});
auto registered_zero = context->get_constant_mgr()->FindConstant(&zero);
if (!registered_zero) {
// We need 0 in order to be able to initialize loop limiters.
return false;
}
uint32_t zero_id = context->get_constant_mgr()
->GetDefiningInstruction(registered_zero)
->result_id();
// Look for 1 of type unsigned int.
opt::analysis::IntConstant one(registered_unsigned_int_type->AsInteger(),
{1});
auto registered_one = context->get_constant_mgr()->FindConstant(&one);
if (!registered_one) {
// We need 1 in order to be able to increment loop limiters.
return false;
}
uint32_t one_id = context->get_constant_mgr()
->GetDefiningInstruction(registered_one)
->result_id();
// Look for pointer-to-unsigned int type.
opt::analysis::Pointer pointer_to_unsigned_int_type(
registered_unsigned_int_type, SpvStorageClassFunction);
uint32_t pointer_to_unsigned_int_type_id =
context->get_type_mgr()->GetId(&pointer_to_unsigned_int_type);
if (!pointer_to_unsigned_int_type_id) {
// We need pointer-to-unsigned int in order to declare the loop limiter
// variable.
return false;
}
// Look for bool type.
opt::analysis::Bool bool_type;
uint32_t bool_type_id = context->get_type_mgr()->GetId(&bool_type);
if (!bool_type_id) {
// We need bool in order to compare the loop limiter's value with the loop
// limit constant.
return false;
}
// Declare the loop limiter variable at the start of the function's entry
// block, via an instruction of the form:
// %loop_limiter_var = SpvOpVariable %ptr_to_uint Function %zero
added_function->begin()->begin()->InsertBefore(MakeUnique<opt::Instruction>(
context, SpvOpVariable, pointer_to_unsigned_int_type_id,
message_.loop_limiter_variable_id(),
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_STORAGE_CLASS, {SpvStorageClassFunction}},
{SPV_OPERAND_TYPE_ID, {zero_id}}})));
// Update the module's id bound since we have added the loop limiter
// variable id.
fuzzerutil::UpdateModuleIdBound(context, message_.loop_limiter_variable_id());
// Consider each loop in turn.
for (auto loop_header : loop_headers) {
// Look for the loop's back-edge block. This is a predecessor of the loop
// header that is dominated by the loop header.
uint32_t back_edge_block_id = 0;
for (auto pred : context->cfg()->preds(loop_header->id())) {
if (context->GetDominatorAnalysis(added_function)
->Dominates(loop_header->id(), pred)) {
back_edge_block_id = pred;
break;
}
}
if (!back_edge_block_id) {
// The loop's back-edge block must be unreachable. This means that the
// loop cannot iterate, so there is no need to make it lifesafe; we can
// move on from this loop.
continue;
}
auto back_edge_block = context->cfg()->block(back_edge_block_id);
// Go through the sequence of loop limiter infos and find the one
// corresponding to this loop.
bool found = false;
protobufs::LoopLimiterInfo loop_limiter_info;
for (auto& info : message_.loop_limiter_info()) {
if (info.loop_header_id() == loop_header->id()) {
loop_limiter_info = info;
found = true;
break;
}
}
if (!found) {
// We don't have loop limiter info for this loop header.
return false;
}
// The back-edge block either has the form:
//
// (1)
//
// %l = OpLabel
// ... instructions ...
// OpBranch %loop_header
//
// (2)
//
// %l = OpLabel
// ... instructions ...
// OpBranchConditional %c %loop_header %loop_merge
//
// (3)
//
// %l = OpLabel
// ... instructions ...
// OpBranchConditional %c %loop_merge %loop_header
//
// We turn these into the following:
//
// (1)
//
// %l = OpLabel
// ... instructions ...
// %t1 = OpLoad %uint32 %loop_limiter
// %t2 = OpIAdd %uint32 %t1 %one
// OpStore %loop_limiter %t2
// %t3 = OpUGreaterThanEqual %bool %t1 %loop_limit
// OpBranchConditional %t3 %loop_merge %loop_header
//
// (2)
//
// %l = OpLabel
// ... instructions ...
// %t1 = OpLoad %uint32 %loop_limiter
// %t2 = OpIAdd %uint32 %t1 %one
// OpStore %loop_limiter %t2
// %t3 = OpULessThan %bool %t1 %loop_limit
// %t4 = OpLogicalAnd %bool %c %t3
// OpBranchConditional %t4 %loop_header %loop_merge
//
// (3)
//
// %l = OpLabel
// ... instructions ...
// %t1 = OpLoad %uint32 %loop_limiter
// %t2 = OpIAdd %uint32 %t1 %one
// OpStore %loop_limiter %t2
// %t3 = OpUGreaterThanEqual %bool %t1 %loop_limit
// %t4 = OpLogicalOr %bool %c %t3
// OpBranchConditional %t4 %loop_merge %loop_header
auto back_edge_block_terminator = back_edge_block->terminator();
bool compare_using_greater_than_equal;
if (back_edge_block_terminator->opcode() == SpvOpBranch) {
compare_using_greater_than_equal = true;
} else {
assert(back_edge_block_terminator->opcode() == SpvOpBranchConditional);
assert(((back_edge_block_terminator->GetSingleWordInOperand(1) ==
loop_header->id() &&
back_edge_block_terminator->GetSingleWordInOperand(2) ==
loop_header->MergeBlockId()) ||
(back_edge_block_terminator->GetSingleWordInOperand(2) ==
loop_header->id() &&
back_edge_block_terminator->GetSingleWordInOperand(1) ==
loop_header->MergeBlockId())) &&
"A back edge edge block must branch to"
" either the loop header or merge");
compare_using_greater_than_equal =
back_edge_block_terminator->GetSingleWordInOperand(1) ==
loop_header->MergeBlockId();
}
std::vector<std::unique_ptr<opt::Instruction>> new_instructions;
// Add a load from the loop limiter variable, of the form:
// %t1 = OpLoad %uint32 %loop_limiter
new_instructions.push_back(MakeUnique<opt::Instruction>(
context, SpvOpLoad, unsigned_int_type_id, loop_limiter_info.load_id(),
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID, {message_.loop_limiter_variable_id()}}})));
// Increment the loaded value:
// %t2 = OpIAdd %uint32 %t1 %one
new_instructions.push_back(MakeUnique<opt::Instruction>(
context, SpvOpIAdd, unsigned_int_type_id,
loop_limiter_info.increment_id(),
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID, {loop_limiter_info.load_id()}},
{SPV_OPERAND_TYPE_ID, {one_id}}})));
// Store the incremented value back to the loop limiter variable:
// OpStore %loop_limiter %t2
new_instructions.push_back(MakeUnique<opt::Instruction>(
context, SpvOpStore, 0, 0,
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID, {message_.loop_limiter_variable_id()}},
{SPV_OPERAND_TYPE_ID, {loop_limiter_info.increment_id()}}})));
// Compare the loaded value with the loop limit; either:
// %t3 = OpUGreaterThanEqual %bool %t1 %loop_limit
// or
// %t3 = OpULessThan %bool %t1 %loop_limit
new_instructions.push_back(MakeUnique<opt::Instruction>(
context,
compare_using_greater_than_equal ? SpvOpUGreaterThanEqual
: SpvOpULessThan,
bool_type_id, loop_limiter_info.compare_id(),
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID, {loop_limiter_info.load_id()}},
{SPV_OPERAND_TYPE_ID, {message_.loop_limit_constant_id()}}})));
if (back_edge_block_terminator->opcode() == SpvOpBranchConditional) {
new_instructions.push_back(MakeUnique<opt::Instruction>(
context,
compare_using_greater_than_equal ? SpvOpLogicalOr : SpvOpLogicalAnd,
bool_type_id, loop_limiter_info.logical_op_id(),
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID,
{back_edge_block_terminator->GetSingleWordInOperand(0)}},
{SPV_OPERAND_TYPE_ID, {loop_limiter_info.compare_id()}}})));
}
// Add the new instructions at the end of the back edge block, before the
// terminator and any loop merge instruction (as the back edge block can
// be the loop header).
if (back_edge_block->GetLoopMergeInst()) {
back_edge_block->GetLoopMergeInst()->InsertBefore(
std::move(new_instructions));
} else {
back_edge_block_terminator->InsertBefore(std::move(new_instructions));
}
if (back_edge_block_terminator->opcode() == SpvOpBranchConditional) {
back_edge_block_terminator->SetInOperand(
0, {loop_limiter_info.logical_op_id()});
} else {
assert(back_edge_block_terminator->opcode() == SpvOpBranch &&
"Back-edge terminator must be OpBranch or OpBranchConditional");
// Check that, if the merge block starts with OpPhi instructions, suitable
// ids have been provided to give these instructions a value corresponding
// to the new incoming edge from the back edge block.
auto merge_block = context->cfg()->block(loop_header->MergeBlockId());
if (!fuzzerutil::PhiIdsOkForNewEdge(context, back_edge_block, merge_block,
loop_limiter_info.phi_id())) {
return false;
}
// Augment OpPhi instructions at the loop merge with the given ids.
uint32_t phi_index = 0;
for (auto& inst : *merge_block) {
if (inst.opcode() != SpvOpPhi) {
break;
}
assert(phi_index <
static_cast<uint32_t>(loop_limiter_info.phi_id().size()) &&
"There should be at least one phi id per OpPhi instruction.");
inst.AddOperand(
{SPV_OPERAND_TYPE_ID, {loop_limiter_info.phi_id(phi_index)}});
inst.AddOperand({SPV_OPERAND_TYPE_ID, {back_edge_block_id}});
phi_index++;
}
// Add the new edge, by changing OpBranch to OpBranchConditional.
// TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3162): This
// could be a problem if the merge block was originally unreachable: it
// might now be dominated by other blocks that it appears earlier than in
// the module.
back_edge_block_terminator->SetOpcode(SpvOpBranchConditional);
back_edge_block_terminator->SetInOperands(opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID, {loop_limiter_info.compare_id()}},
{SPV_OPERAND_TYPE_ID, {loop_header->MergeBlockId()}
},
{SPV_OPERAND_TYPE_ID, {loop_header->id()}}}));
}
// Update the module's id bound with respect to the various ids that
// have been used for loop limiter manipulation.
fuzzerutil::UpdateModuleIdBound(context, loop_limiter_info.load_id());
fuzzerutil::UpdateModuleIdBound(context, loop_limiter_info.increment_id());
fuzzerutil::UpdateModuleIdBound(context, loop_limiter_info.compare_id());
fuzzerutil::UpdateModuleIdBound(context, loop_limiter_info.logical_op_id());
}
return true;
}
bool TransformationAddFunction::TryToTurnKillOrUnreachableIntoReturn(
opt::IRContext* context, opt::Function* added_function,
opt::Instruction* kill_or_unreachable_inst) const {
assert((kill_or_unreachable_inst->opcode() == SpvOpKill ||
kill_or_unreachable_inst->opcode() == SpvOpUnreachable) &&
"Precondition: instruction must be OpKill or OpUnreachable.");
// Get the function's return type.
auto function_return_type_inst =
context->get_def_use_mgr()->GetDef(added_function->type_id());
if (function_return_type_inst->opcode() == SpvOpTypeVoid) {
// The function has void return type, so change this instruction to
// OpReturn.
kill_or_unreachable_inst->SetOpcode(SpvOpReturn);
} else {
// The function has non-void return type, so change this instruction
// to OpReturnValue, using the value id provided with the
// transformation.
// We first check that the id, %id, provided with the transformation
// specifically to turn OpKill and OpUnreachable instructions into
// OpReturnValue %id has the same type as the function's return type.
if (context->get_def_use_mgr()
->GetDef(message_.kill_unreachable_return_value_id())
->type_id() != function_return_type_inst->result_id()) {
return false;
}
kill_or_unreachable_inst->SetOpcode(SpvOpReturnValue);
kill_or_unreachable_inst->SetInOperands(
{{SPV_OPERAND_TYPE_ID, {message_.kill_unreachable_return_value_id()}}});
}
return true;
}
bool TransformationAddFunction::TryToClampAccessChainIndices(
opt::IRContext* context, opt::Instruction* access_chain_inst) const {
assert((access_chain_inst->opcode() == SpvOpAccessChain ||
access_chain_inst->opcode() == SpvOpInBoundsAccessChain) &&
"Precondition: instruction must be OpAccessChain or "
"OpInBoundsAccessChain.");
// Find the AccessChainClampingInfo associated with this access chain.
const protobufs::AccessChainClampingInfo* access_chain_clamping_info =
nullptr;
for (auto& clamping_info : message_.access_chain_clamping_info()) {
if (clamping_info.access_chain_id() == access_chain_inst->result_id()) {
access_chain_clamping_info = &clamping_info;
break;
}
}
if (!access_chain_clamping_info) {
// No access chain clamping information was found; the function cannot be
// made livesafe.
return false;
}
// Check that there is a (compare_id, select_id) pair for every
// index associated with the instruction.
if (static_cast<uint32_t>(
access_chain_clamping_info->compare_and_select_ids().size()) !=
access_chain_inst->NumInOperands() - 1) {
return false;
}
// Walk the access chain, clamping each index to be within bounds if it is
// not a constant.
auto base_object = context->get_def_use_mgr()->GetDef(
access_chain_inst->GetSingleWordInOperand(0));
assert(base_object && "The base object must exist.");
auto pointer_type =
context->get_def_use_mgr()->GetDef(base_object->type_id());
assert(pointer_type && pointer_type->opcode() == SpvOpTypePointer &&
"The base object must have pointer type.");
auto should_be_composite_type = context->get_def_use_mgr()->GetDef(
pointer_type->GetSingleWordInOperand(1));
// Consider each index input operand in turn (operand 0 is the base object).
for (uint32_t index = 1; index < access_chain_inst->NumInOperands();
index++) {
// We are going to turn:
//
// %result = OpAccessChain %type %object ... %index ...
//
// into:
//
// %t1 = OpULessThanEqual %bool %index %bound_minus_one
// %t2 = OpSelect %int_type %t1 %index %bound_minus_one
// %result = OpAccessChain %type %object ... %t2 ...
//
// ... unless %index is already a constant.
// Get the bound for the composite being indexed into; e.g. the number of
// columns of matrix or the size of an array.
uint32_t bound =
GetBoundForCompositeIndex(context, *should_be_composite_type);
// Get the instruction associated with the index and figure out its integer
// type.
const uint32_t index_id = access_chain_inst->GetSingleWordInOperand(index);
auto index_inst = context->get_def_use_mgr()->GetDef(index_id);
auto index_type_inst =
context->get_def_use_mgr()->GetDef(index_inst->type_id());
assert(index_type_inst->opcode() == SpvOpTypeInt);
assert(index_type_inst->GetSingleWordInOperand(0) == 32);
opt::analysis::Integer* index_int_type =
context->get_type_mgr()
->GetType(index_type_inst->result_id())
->AsInteger();
if (index_inst->opcode() != SpvOpConstant) {
// The index is non-constant so we need to clamp it.
assert(should_be_composite_type->opcode() != SpvOpTypeStruct &&
"Access chain indices into structures are required to be "
"constants.");
opt::analysis::IntConstant bound_minus_one(index_int_type, {bound - 1});
if (!context->get_constant_mgr()->FindConstant(&bound_minus_one)) {
// We do not have an integer constant whose value is |bound| -1.
return false;
}
opt::analysis::Bool bool_type;
uint32_t bool_type_id = context->get_type_mgr()->GetId(&bool_type);
if (!bool_type_id) {
// Bool type is not declared; we cannot do a comparison.
return false;
}
uint32_t bound_minus_one_id =
context->get_constant_mgr()
->GetDefiningInstruction(&bound_minus_one)
->result_id();
uint32_t compare_id =
access_chain_clamping_info->compare_and_select_ids(index - 1).first();
uint32_t select_id =
access_chain_clamping_info->compare_and_select_ids(index - 1)
.second();
std::vector<std::unique_ptr<opt::Instruction>> new_instructions;
// Compare the index with the bound via an instruction of the form:
// %t1 = OpULessThanEqual %bool %index %bound_minus_one
new_instructions.push_back(MakeUnique<opt::Instruction>(
context, SpvOpULessThanEqual, bool_type_id, compare_id,
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID, {index_inst->result_id()}},
{SPV_OPERAND_TYPE_ID, {bound_minus_one_id}}})));
// Select the index if in-bounds, otherwise one less than the bound:
// %t2 = OpSelect %int_type %t1 %index %bound_minus_one
new_instructions.push_back(MakeUnique<opt::Instruction>(
context, SpvOpSelect, index_type_inst->result_id(), select_id,
opt::Instruction::OperandList(
{{SPV_OPERAND_TYPE_ID, {compare_id}},
{SPV_OPERAND_TYPE_ID, {index_inst->result_id()}},
{SPV_OPERAND_TYPE_ID, {bound_minus_one_id}}})));
// Add the new instructions before the access chain
access_chain_inst->InsertBefore(std::move(new_instructions));
// Replace %index with %t2.
access_chain_inst->SetInOperand(index, {select_id});
fuzzerutil::UpdateModuleIdBound(context, compare_id);
fuzzerutil::UpdateModuleIdBound(context, select_id);
} else {
// TODO(afd): At present the SPIR-V spec is not clear on whether
// statically out-of-bounds indices mean that a module is invalid (so
// that it should be rejected by the validator), or that such accesses
// yield undefined results. Via the following assertion, we assume that
// functions added to the module do not feature statically out-of-bounds
// accesses.
// Assert that the index is smaller (unsigned) than this value.
// Return false if it is not (to keep compilers happy).
if (index_inst->GetSingleWordInOperand(0) >= bound) {
assert(false &&
"The function has a statically out-of-bounds access; "
"this should not occur.");
return false;
}
}
should_be_composite_type =
FollowCompositeIndex(context, *should_be_composite_type, index_id);
}
return true;
}
uint32_t TransformationAddFunction::GetBoundForCompositeIndex(
opt::IRContext* context, const opt::Instruction& composite_type_inst) {
switch (composite_type_inst.opcode()) {
case SpvOpTypeArray:
return fuzzerutil::GetArraySize(composite_type_inst, context);
case SpvOpTypeMatrix:
case SpvOpTypeVector:
return composite_type_inst.GetSingleWordInOperand(1);
case SpvOpTypeStruct: {
return fuzzerutil::GetNumberOfStructMembers(composite_type_inst);
}
default:
assert(false && "Unknown composite type.");
return 0;
}
}
opt::Instruction* TransformationAddFunction::FollowCompositeIndex(
opt::IRContext* context, const opt::Instruction& composite_type_inst,
uint32_t index_id) {
uint32_t sub_object_type_id;
switch (composite_type_inst.opcode()) {
case SpvOpTypeArray:
sub_object_type_id = composite_type_inst.GetSingleWordInOperand(0);
break;
case SpvOpTypeMatrix:
case SpvOpTypeVector:
sub_object_type_id = composite_type_inst.GetSingleWordInOperand(0);
break;
case SpvOpTypeStruct: {
auto index_inst = context->get_def_use_mgr()->GetDef(index_id);
assert(index_inst->opcode() == SpvOpConstant);
assert(
context->get_def_use_mgr()->GetDef(index_inst->type_id())->opcode() ==
SpvOpTypeInt);
assert(context->get_def_use_mgr()
->GetDef(index_inst->type_id())
->GetSingleWordInOperand(0) == 32);
uint32_t index_value = index_inst->GetSingleWordInOperand(0);
sub_object_type_id =
composite_type_inst.GetSingleWordInOperand(index_value);
break;
}
default:
assert(false && "Unknown composite type.");
sub_object_type_id = 0;
break;
}
assert(sub_object_type_id && "No sub-object found.");
return context->get_def_use_mgr()->GetDef(sub_object_type_id);
}
} // namespace fuzz
} // namespace spvtools

Просмотреть файл

@ -28,26 +28,56 @@ class TransformationAddFunction : public Transformation {
explicit TransformationAddFunction(
const protobufs::TransformationAddFunction& message);
// Creates a transformation to add a non live-safe function.
explicit TransformationAddFunction(
const std::vector<protobufs::Instruction>& instructions);
// Creates a transformation to add a live-safe function.
TransformationAddFunction(
const std::vector<protobufs::Instruction>& instructions,
uint32_t loop_limiter_variable_id, uint32_t loop_limit_constant_id,
const std::vector<protobufs::LoopLimiterInfo>& loop_limiters,
uint32_t kill_unreachable_return_value_id,
const std::vector<protobufs::AccessChainClampingInfo>&
access_chain_clampers);
// - |message_.instruction| must correspond to a sufficiently well-formed
// sequence of instructions that a function can be created from them
// - If |message_.is_livesafe| holds then |message_| must contain suitable
// ingredients to make the function livesafe, and the function must only
// invoke other livesafe functions
// - Adding the created function to the module must lead to a valid module.
bool IsApplicable(opt::IRContext* context,
const FactManager& fact_manager) const override;
// Adds the function defined by |message_.instruction| to the module
// Adds the function defined by |message_.instruction| to the module, making
// it livesafe if |message_.is_livesafe| holds.
void Apply(opt::IRContext* context, FactManager* fact_manager) const override;
protobufs::Transformation ToMessage() const override;
// Helper method that returns the bound for indexing into a composite of type
// |composite_type_inst|, i.e. the number of fields of a struct, the size of
// an array, the number of components of a vector, or the number of columns of
// a matrix.
static uint32_t GetBoundForCompositeIndex(
opt::IRContext* context, const opt::Instruction& composite_type_inst);
// Helper method that, given composite type |composite_type_inst|, returns the
// type of the sub-object at index |index_id|, which is required to be in-
// bounds.
static opt::Instruction* FollowCompositeIndex(
opt::IRContext* context, const opt::Instruction& composite_type_inst,
uint32_t index_id);
private:
// Attempts to create a function from the series of instructions in
// |message_.instruction| and add it to |context|. Returns false if this is
// not possible due to the messages not respecting the basic structure of a
// function, e.g. if there is no OpFunction instruction or no blocks; in this
// case |context| is left in an indeterminate state.
// |message_.instruction| and add it to |context|.
//
// Returns false if adding the function is not possible due to the messages
// not respecting the basic structure of a function, e.g. if there is no
// OpFunction instruction or no blocks; in this case |context| is left in an
// indeterminate state.
//
// Otherwise returns true. Whether |context| is valid after addition of the
// function depends on the contents of |message_.instruction|.
@ -61,6 +91,30 @@ class TransformationAddFunction : public Transformation {
// to add the function.
bool TryToAddFunction(opt::IRContext* context) const;
// Should only be called if |message_.is_livesafe| holds. Attempts to make
// the function livesafe (see FactFunctionIsLivesafe for a definition).
// Returns false if this is not possible, due to |message_| or |context| not
// containing sufficient ingredients (such as types and fresh ids) to add
// the instrumentation necessary to make the function livesafe.
bool TryToMakeFunctionLivesafe(opt::IRContext* context,
const FactManager& fact_manager) const;
// A helper for TryToMakeFunctionLivesafe that tries to add loop-limiting
// logic.
bool TryToAddLoopLimiters(opt::IRContext* context,
opt::Function* added_function) const;
// A helper for TryToMakeFunctionLivesafe that tries to replace OpKill and
// OpUnreachable instructions into return instructions.
bool TryToTurnKillOrUnreachableIntoReturn(
opt::IRContext* context, opt::Function* added_function,
opt::Instruction* kill_or_unreachable_inst) const;
// A helper for TryToMakeFunctionLivesafe that tries to clamp access chain
// indices so that they are guaranteed to be in-bounds.
bool TryToClampAccessChainIndices(opt::IRContext* context,
opt::Instruction* access_chain_inst) const;
protobufs::TransformationAddFunction message_;
};

Просмотреть файл

@ -254,11 +254,21 @@ bool TransformationOutlineFunction::IsApplicable(
if (input_id_to_fresh_id_map.count(id) == 0) {
return false;
}
// Furthermore, no region input id is allowed to be the result of an access
// chain. This is because region input ids will become function parameters,
// and it is not legal to pass an access chain as a function parameter.
if (context->get_def_use_mgr()->GetDef(id)->opcode() == SpvOpAccessChain) {
return false;
// Furthermore, if the input id has pointer type it must be an OpVariable
// or OpFunctionParameter.
auto input_id_inst = context->get_def_use_mgr()->GetDef(id);
if (context->get_def_use_mgr()
->GetDef(input_id_inst->type_id())
->opcode() == SpvOpTypePointer) {
switch (input_id_inst->opcode()) {
case SpvOpFunctionParameter:
case SpvOpVariable:
// These are OK.
break;
default:
// Anything else is not OK.
return false;
}
}
}

Просмотреть файл

@ -195,14 +195,15 @@ TEST(FuzzerPassDonateModulesTest, BasicDonation) {
FactManager fact_manager;
FuzzerContext fuzzer_context(MakeUnique<PseudoRandomGenerator>(0).get(), 100);
auto prng = MakeUnique<PseudoRandomGenerator>(0);
FuzzerContext fuzzer_context(prng.get(), 100);
protobufs::TransformationSequence transformation_sequence;
FuzzerPassDonateModules fuzzer_pass(recipient_context.get(), &fact_manager,
&fuzzer_context, &transformation_sequence,
{});
fuzzer_pass.DonateSingleModule(donor_context.get());
fuzzer_pass.DonateSingleModule(donor_context.get(), false);
// We just check that the result is valid. Checking to what it should be
// exactly equal to would be very fragile.
@ -276,7 +277,7 @@ TEST(FuzzerPassDonateModulesTest, DonationWithUniforms) {
&fuzzer_context, &transformation_sequence,
{});
fuzzer_pass.DonateSingleModule(donor_context.get());
fuzzer_pass.DonateSingleModule(donor_context.get(), false);
ASSERT_TRUE(IsValid(env, recipient_context.get()));
@ -397,7 +398,7 @@ TEST(FuzzerPassDonateModulesTest, DonationWithInputAndOutputVariables) {
&fuzzer_context, &transformation_sequence,
{});
fuzzer_pass.DonateSingleModule(donor_context.get());
fuzzer_pass.DonateSingleModule(donor_context.get(), false);
ASSERT_TRUE(IsValid(env, recipient_context.get()));
@ -483,7 +484,7 @@ TEST(FuzzerPassDonateModulesTest, DonateFunctionTypeWithDifferentPointers) {
&fuzzer_context, &transformation_sequence,
{});
fuzzer_pass.DonateSingleModule(donor_context.get());
fuzzer_pass.DonateSingleModule(donor_context.get(), false);
// We just check that the result is valid. Checking to what it should be
// exactly equal to would be very fragile.
@ -660,7 +661,7 @@ TEST(FuzzerPassDonateModulesTest, Miscellaneous1) {
&fuzzer_context, &transformation_sequence,
{});
fuzzer_pass.DonateSingleModule(donor_context.get());
fuzzer_pass.DonateSingleModule(donor_context.get(), false);
// We just check that the result is valid. Checking to what it should be
// exactly equal to would be very fragile.

Просмотреть файл

@ -1948,9 +1948,6 @@ TEST(TransformationAddDeadBreakTest, PhiInstructions) {
// Not applicable because two OpPhis (not just one) need to be updated at 20
ASSERT_FALSE(TransformationAddDeadBreak(23, 20, true, {13})
.IsApplicable(context.get(), fact_manager));
// Not applicable because only two OpPhis (not three) need to be updated at 20
ASSERT_FALSE(TransformationAddDeadBreak(23, 20, true, {13, 21, 22})
.IsApplicable(context.get(), fact_manager));
// Not applicable because the given ids do not have types that match the
// OpPhis at 20, in order
ASSERT_FALSE(TransformationAddDeadBreak(23, 20, true, {21, 13})

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1656,6 +1656,64 @@ TEST(TransformationOutlineFunctionTest, DoNotOutlineRegionThatUsesAccessChain) {
ASSERT_FALSE(transformation.IsApplicable(context.get(), fact_manager));
}
TEST(TransformationOutlineFunctionTest,
DoNotOutlineRegionThatUsesCopiedObject) {
// Copying a variable leads to a pointer, but one that cannot be passed as a
// function parameter, as it is not a memory object.
std::string shader = R"(
OpCapability Shader
%1 = OpExtInstImport "GLSL.std.450"
OpMemoryModel Logical GLSL450
OpEntryPoint Fragment %4 "main"
OpExecutionMode %4 OriginUpperLeft
OpSource ESSL 310
OpName %4 "main"
%2 = OpTypeVoid
%3 = OpTypeFunction %2
%6 = OpTypeFloat 32
%7 = OpTypeVector %6 4
%8 = OpTypePointer Function %7
%9 = OpTypePointer Function %6
%18 = OpTypeInt 32 0
%19 = OpConstant %18 0
%4 = OpFunction %2 None %3
%5 = OpLabel
%10 = OpVariable %8 Function
OpBranch %11
%11 = OpLabel
%20 = OpCopyObject %8 %10
OpBranch %13
%13 = OpLabel
%12 = OpAccessChain %9 %20 %19
%14 = OpLoad %6 %12
OpBranch %15
%15 = OpLabel
OpReturn
OpFunctionEnd
)";
const auto env = SPV_ENV_UNIVERSAL_1_5;
const auto consumer = nullptr;
const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
ASSERT_TRUE(IsValid(env, context.get()));
FactManager fact_manager;
TransformationOutlineFunction transformation(
/*entry_block*/ 13,
/*exit_block*/ 15,
/*new_function_struct_return_type_id*/ 200,
/*new_function_type_id*/ 201,
/*new_function_id*/ 202,
/*new_function_region_entry_block*/ 204,
/*new_caller_result_id*/ 205,
/*new_callee_result_id*/ 206,
/*input_id_to_fresh_id*/ {{20, 207}},
/*output_id_to_fresh_id*/ {});
ASSERT_FALSE(transformation.IsApplicable(context.get(), fact_manager));
}
TEST(TransformationOutlineFunctionTest, Miscellaneous1) {
// This tests outlining of some non-trivial code.