SPIRV-Tools/source/fuzz/shrinker.cpp
Alastair Donaldson b8ab80843f
Shrinker for spirv-fuzz (#2708)
Adds to spirv-fuzz the option to shrink a sequence of transformations
that lead to an interesting binary to be generated, to find a smaller
sub-sequence of transformations that still lead to an interesting (but
hopefully simpler) binary being generated. The notion of what counts
as "interesting" comes from a user-provided script, the
"interestingness function", similar to the way the spirv-reduce tool
works. The shrinking process will give up after a maximum number of
steps, which can be configured on the command line.

Tests for the combination of fuzzing and shrinking are included, using
a variety of interestingness functions.
2019-07-07 08:55:30 +01:00

241 lines
9.8 KiB
C++

// 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/shrinker.h"
#include <sstream>
#include "source/fuzz/pseudo_random_generator.h"
#include "source/fuzz/replayer.h"
#include "source/spirv_fuzzer_options.h"
#include "source/util/make_unique.h"
namespace spvtools {
namespace fuzz {
namespace {
// A helper to get the size of a protobuf transformation sequence in a less
// verbose manner.
uint32_t NumRemainingTransformations(
const protobufs::TransformationSequence& transformation_sequence) {
return static_cast<uint32_t>(transformation_sequence.transformation_size());
}
// A helper to return a transformation sequence identical to |transformations|,
// except that a chunk of size |chunk_size| starting from |chunk_index| x
// |chunk_size| is removed (or as many transformations as available if the whole
// chunk is not).
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;
}
} // namespace
struct Shrinker::Impl {
explicit Impl(spv_target_env env, uint32_t limit)
: target_env(env), step_limit(limit) {}
const spv_target_env target_env; // Target environment.
MessageConsumer consumer; // Message consumer.
const uint32_t step_limit; // Step limit for reductions.
};
Shrinker::Shrinker(spv_target_env env, uint32_t step_limit)
: impl_(MakeUnique<Impl>(env, step_limit)) {}
Shrinker::~Shrinker() = default;
void Shrinker::SetMessageConsumer(MessageConsumer c) {
impl_->consumer = std::move(c);
}
Shrinker::ShrinkerResultStatus Shrinker::Run(
const std::vector<uint32_t>& binary_in,
const protobufs::FactSequence& initial_facts,
const protobufs::TransformationSequence& transformation_sequence_in,
const Shrinker::InterestingnessFunction& interestingness_function,
std::vector<uint32_t>* binary_out,
protobufs::TransformationSequence* transformation_sequence_out) const {
// Check compatibility between the library version being linked with and the
// header files being used.
GOOGLE_PROTOBUF_VERIFY_VERSION;
spvtools::SpirvTools tools(impl_->target_env);
if (!tools.IsValid()) {
impl_->consumer(SPV_MSG_ERROR, nullptr, {},
"Failed to create SPIRV-Tools interface; stopping.");
return Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface;
}
// Initial binary should be valid.
if (!tools.Validate(&binary_in[0], binary_in.size())) {
impl_->consumer(SPV_MSG_INFO, nullptr, {},
"Initial binary is invalid; stopping.");
return Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid;
}
std::vector<uint32_t> current_best_binary;
protobufs::TransformationSequence current_best_transformations;
// Run a replay of the initial transformation sequence to (a) check that it
// succeeds, (b) get the binary that results from running these
// transformations, and (c) get the subsequence of the initial transformations
// that actually apply (in principle this could be a strict subsequence).
if (Replayer(impl_->target_env)
.Run(binary_in, initial_facts, transformation_sequence_in,
&current_best_binary, &current_best_transformations) !=
Replayer::ReplayerResultStatus::kComplete) {
return ShrinkerResultStatus::kReplayFailed;
}
// Check that the binary produced by applying the initial transformations is
// indeed interesting.
if (!interestingness_function(current_best_binary, 0)) {
impl_->consumer(SPV_MSG_INFO, nullptr, {},
"Initial binary is not interesting; stopping.");
return ShrinkerResultStatus::kInitialBinaryNotInteresting;
}
uint32_t attempt = 0; // Keeps track of the number of shrink attempts that
// have been tried, whether successful or not.
uint32_t chunk_size =
std::max(1u, NumRemainingTransformations(current_best_transformations) /
2); // The number of contiguous transformations that the
// shrinker will try to remove in one go; starts
// high and decreases during the shrinking process.
// Keep shrinking until we:
// - reach the step limit,
// - run out of transformations to remove, or
// - cannot make the chunk size any smaller.
while (attempt < impl_->step_limit &&
!current_best_transformations.transformation().empty() &&
chunk_size > 0) {
bool progress_this_round =
false; // Used to decide whether to make the chunk size with which we
// remove transformations smaller. If we managed to remove at
// least one chunk of transformations at a particular chunk
// size, we set this flag so that we do not yet decrease the
// chunk size.
assert(chunk_size <=
NumRemainingTransformations(current_best_transformations) &&
"Chunk size should never exceed the number of transformations that "
"remain.");
// The number of chunks is the ceiling of (#remaining_transformations /
// chunk_size).
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.");
// We go through the transformations in reverse, in chunks of size
// |chunk_size|, using |chunk_index| to track which chunk to try removing
// next. The loop exits early if we reach the shrinking step limit.
for (int chunk_index = num_chunks - 1;
attempt < impl_->step_limit && chunk_index >= 0; chunk_index--) {
// Remove a chunk of transformations according to the current index and
// chunk size.
auto transformations_with_chunk_removed =
RemoveChunk(current_best_transformations, chunk_index, chunk_size);
// Replay the smaller sequence of transformations to get a next binary and
// transformation sequence. Note that the transformations arising from
// replay might be even smaller than the transformations with the chunk
// removed, because removing those transformations might make further
// transformations inapplicable.
std::vector<uint32_t> next_binary;
protobufs::TransformationSequence next_transformation_sequence;
if (Replayer(impl_->target_env)
.Run(binary_in, initial_facts, transformations_with_chunk_removed,
&next_binary, &next_transformation_sequence) !=
Replayer::ReplayerResultStatus::kComplete) {
// Replay should not fail; if it does, we need to abort shrinking.
return ShrinkerResultStatus::kReplayFailed;
}
assert(NumRemainingTransformations(next_transformation_sequence) >=
chunk_index * chunk_size &&
"Removing this chunk of transformations should not have an effect "
"on earlier chunks.");
if (interestingness_function(next_binary, attempt)) {
// If the binary arising from the smaller transformation sequence is
// interesting, this becomes our current best binary and transformation
// sequence.
current_best_binary = next_binary;
current_best_transformations = next_transformation_sequence;
progress_this_round = true;
}
// Either way, this was a shrink attempt, so increment our count of shrink
// attempts.
attempt++;
}
if (!progress_this_round) {
// If we didn't manage to remove any chunks at this chunk size, try a
// smaller chunk size.
chunk_size /= 2;
}
// Decrease the chunk size until it becomes no larger than the number of
// remaining transformations.
while (chunk_size >
NumRemainingTransformations(current_best_transformations)) {
chunk_size /= 2;
}
}
// The output from the shrinker is the best binary we saw, and the
// transformations that led to it.
*binary_out = current_best_binary;
*transformation_sequence_out = current_best_transformations;
// Indicate whether shrinking completed or was truncated due to reaching the
// step limit.
assert(attempt <= impl_->step_limit);
if (attempt == impl_->step_limit) {
std::stringstream strstream;
strstream << "Shrinking did not complete; step limit " << impl_->step_limit
<< " was reached.";
impl_->consumer(SPV_MSG_WARNING, nullptr, {}, strstream.str().c_str());
return Shrinker::ShrinkerResultStatus::kStepLimitReached;
}
return Shrinker::ShrinkerResultStatus::kComplete;
}
} // namespace fuzz
} // namespace spvtools