#include "source/fuzz/shrinker.h"
#include <sstream>
#include "source/fuzz/added_function_reducer.h"
#include "source/fuzz/pseudo_random_generator.h"
#include "source/fuzz/replayer.h"
#include "source/opt/build_module.h"
#include "source/opt/ir_context.h"
#include "source/spirv_fuzzer_options.h"
#include "source/util/make_unique.h"
namespace spvtools {
namespace fuzz {
namespace {
uint32_t NumRemainingTransformations(
const protobufs::TransformationSequence& transformation_sequence) {
return static_cast<uint32_t>(transformation_sequence.transformation_size());
}
protobufs::TransformationSequence RemoveChunk(
const protobufs::TransformationSequence& transformations,
uint32_t chunk_index, uint32_t chunk_size) {
uint32_t lower = chunk_index * chunk_size;
uint32_t upper = std::min((chunk_index + 1) * chunk_size,
NumRemainingTransformations(transformations));
assert(lower < upper);
assert(upper <= NumRemainingTransformations(transformations));
protobufs::TransformationSequence result;
for (uint32_t j = 0; j < NumRemainingTransformations(transformations); j++) {
if (j >= lower && j < upper) {
continue;
}
protobufs::Transformation transformation =
transformations.transformation()[j];
*result.mutable_transformation()->Add() = transformation;
}
return result;
}
}
Shrinker::Shrinker(
spv_target_env target_env, MessageConsumer consumer,
const std::vector<uint32_t>& binary_in,
const protobufs::FactSequence& initial_facts,
const protobufs::TransformationSequence& transformation_sequence_in,
const InterestingnessFunction& interestingness_function,
uint32_t step_limit, bool validate_during_replay,
spv_validator_options validator_options)
: target_env_(target_env),
consumer_(std::move(consumer)),
binary_in_(binary_in),
initial_facts_(initial_facts),
transformation_sequence_in_(transformation_sequence_in),
interestingness_function_(interestingness_function),
step_limit_(step_limit),
validate_during_replay_(validate_during_replay),
validator_options_(validator_options) {}
Shrinker::~Shrinker() = default;
Shrinker::ShrinkerResult Shrinker::Run() {
GOOGLE_PROTOBUF_VERIFY_VERSION;
SpirvTools tools(target_env_);
if (!tools.IsValid()) {
consumer_(SPV_MSG_ERROR, nullptr, {},
"Failed to create SPIRV-Tools interface; stopping.");
return {Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface,
std::vector<uint32_t>(), protobufs::TransformationSequence()};
}
if (!tools.Validate(&binary_in_[0], binary_in_.size(), validator_options_)) {
consumer_(SPV_MSG_INFO, nullptr, {},
"Initial binary is invalid; stopping.");
return {Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid,
std::vector<uint32_t>(), protobufs::TransformationSequence()};
}
auto initial_replay_result =
Replayer(target_env_, consumer_, binary_in_, initial_facts_,
transformation_sequence_in_,
static_cast<uint32_t>(
transformation_sequence_in_.transformation_size()),
validate_during_replay_, validator_options_)
.Run();
if (initial_replay_result.status !=
Replayer::ReplayerResultStatus::kComplete) {
return {ShrinkerResultStatus::kReplayFailed, std::vector<uint32_t>(),
protobufs::TransformationSequence()};
}
std::vector<uint32_t> current_best_binary;
initial_replay_result.transformed_module->module()->ToBinary(
¤t_best_binary, false);
protobufs::TransformationSequence current_best_transformations =
std::move(initial_replay_result.applied_transformations);
if (!interestingness_function_(current_best_binary, 0)) {
consumer_(SPV_MSG_INFO, nullptr, {},
"Initial binary is not interesting; stopping.");
return {ShrinkerResultStatus::kInitialBinaryNotInteresting,
std::vector<uint32_t>(), protobufs::TransformationSequence()};
}
uint32_t attempt = 0;
uint32_t chunk_size =
std::max(1u, NumRemainingTransformations(current_best_transformations) /
2);
while (attempt < step_limit_ &&
!current_best_transformations.transformation().empty() &&
chunk_size > 0) {
bool progress_this_round =
false;
assert(chunk_size <=
NumRemainingTransformations(current_best_transformations) &&
"Chunk size should never exceed the number of transformations that "
"remain.");
const uint32_t num_chunks =
(NumRemainingTransformations(current_best_transformations) +
chunk_size - 1) /
chunk_size;
assert(num_chunks >= 1 && "There should be at least one chunk.");
assert(num_chunks * chunk_size >=
NumRemainingTransformations(current_best_transformations) &&
"All transformations should be in some chunk.");
for (int chunk_index = num_chunks - 1;
attempt < step_limit_ && chunk_index >= 0; chunk_index--) {
auto transformations_with_chunk_removed =
RemoveChunk(current_best_transformations,
static_cast<uint32_t>(chunk_index), chunk_size);
auto replay_result =
Replayer(
target_env_, consumer_, binary_in_, initial_facts_,
transformations_with_chunk_removed,
static_cast<uint32_t>(
transformations_with_chunk_removed.transformation_size()),
validate_during_replay_, validator_options_)
.Run();
if (replay_result.status != Replayer::ReplayerResultStatus::kComplete) {
return {ShrinkerResultStatus::kReplayFailed, std::vector<uint32_t>(),
protobufs::TransformationSequence()};
}
assert(
NumRemainingTransformations(replay_result.applied_transformations) >=
chunk_index * chunk_size &&
"Removing this chunk of transformations should not have an effect "
"on earlier chunks.");
std::vector<uint32_t> transformed_binary;
replay_result.transformed_module->module()->ToBinary(&transformed_binary,
false);
if (interestingness_function_(transformed_binary, attempt)) {
current_best_binary = std::move(transformed_binary);
current_best_transformations =
std::move(replay_result.applied_transformations);
progress_this_round = true;
}
attempt++;
}
if (!progress_this_round) {
chunk_size /= 2;
}
while (chunk_size >
NumRemainingTransformations(current_best_transformations)) {
chunk_size /= 2;
}
}
for (uint32_t transformation_index = 0;
attempt < step_limit_ &&
transformation_index <
static_cast<uint32_t>(
current_best_transformations.transformation_size());
transformation_index++) {
if (!current_best_transformations.transformation(transformation_index)
.has_add_function()) {
continue;
}
auto added_function_reducer_result =
AddedFunctionReducer(target_env_, consumer_, binary_in_, initial_facts_,
current_best_transformations, transformation_index,
interestingness_function_, validate_during_replay_,
validator_options_, step_limit_, attempt)
.Run();
if (added_function_reducer_result.status !=
AddedFunctionReducer::AddedFunctionReducerResultStatus::kComplete) {
return {ShrinkerResultStatus::kAddedFunctionReductionFailed,
std::vector<uint32_t>(), protobufs::TransformationSequence()};
}
assert(current_best_transformations.transformation_size() ==
added_function_reducer_result.applied_transformations
.transformation_size() &&
"The number of transformations should not have changed.");
current_best_binary =
std::move(added_function_reducer_result.transformed_binary);
current_best_transformations =
std::move(added_function_reducer_result.applied_transformations);
attempt += added_function_reducer_result.num_reduction_attempts;
}
assert(attempt <= step_limit_);
if (attempt == step_limit_) {
std::stringstream strstream;
strstream << "Shrinking did not complete; step limit " << step_limit_
<< " was reached.";
consumer_(SPV_MSG_WARNING, nullptr, {}, strstream.str().c_str());
return {Shrinker::ShrinkerResultStatus::kStepLimitReached,
std::move(current_best_binary),
std::move(current_best_transformations)};
}
return {Shrinker::ShrinkerResultStatus::kComplete,
std::move(current_best_binary),
std::move(current_best_transformations)};
}
uint32_t Shrinker::GetIdBound(const std::vector<uint32_t>& binary) const {
std::unique_ptr<opt::IRContext> ir_context =
BuildModule(target_env_, consumer_, binary.data(), binary.size());
assert(ir_context && "Error building module.");
return ir_context->module()->id_bound();
}
}
}