mirror of
https://github.com/KhronosGroup/SPIRV-Tools
synced 2025-01-11 09:00:06 +00:00
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.
This commit is contained in:
parent
37e8f79946
commit
b8ab80843f
@ -607,6 +607,11 @@ SPIRV_TOOLS_EXPORT void spvFuzzerOptionsDestroy(spv_fuzzer_options options);
|
||||
SPIRV_TOOLS_EXPORT void spvFuzzerOptionsSetRandomSeed(
|
||||
spv_fuzzer_options options, uint32_t seed);
|
||||
|
||||
// Sets the maximum number of steps that the shrinker should take before giving
|
||||
// up.
|
||||
SPIRV_TOOLS_EXPORT void spvFuzzerOptionsSetShrinkerStepLimit(
|
||||
spv_fuzzer_options options, uint32_t shrinker_step_limit);
|
||||
|
||||
// Encodes the given SPIR-V assembly text to its binary representation. The
|
||||
// length parameter specifies the number of bytes for text. Encoded binary will
|
||||
// be stored into *binary. Any error will be written into *diagnostic if
|
||||
|
@ -207,6 +207,11 @@ class FuzzerOptions {
|
||||
spvFuzzerOptionsSetRandomSeed(options_, seed);
|
||||
}
|
||||
|
||||
// See spvFuzzerOptionsSetShrinkerStepLimit.
|
||||
void set_shrinker_step_limit(uint32_t shrinker_step_limit) {
|
||||
spvFuzzerOptionsSetShrinkerStepLimit(options_, shrinker_step_limit);
|
||||
}
|
||||
|
||||
private:
|
||||
spv_fuzzer_options options_;
|
||||
};
|
||||
|
@ -41,6 +41,7 @@ if(SPIRV_BUILD_FUZZER)
|
||||
pseudo_random_generator.h
|
||||
random_generator.h
|
||||
replayer.h
|
||||
shrinker.h
|
||||
transformation.h
|
||||
transformation_add_constant_boolean.h
|
||||
transformation_add_constant_scalar.h
|
||||
@ -70,6 +71,7 @@ if(SPIRV_BUILD_FUZZER)
|
||||
pseudo_random_generator.cpp
|
||||
random_generator.cpp
|
||||
replayer.cpp
|
||||
shrinker.cpp
|
||||
transformation.cpp
|
||||
transformation_add_constant_boolean.cpp
|
||||
transformation_add_constant_scalar.cpp
|
||||
|
@ -55,14 +55,14 @@ void Fuzzer::SetMessageConsumer(MessageConsumer c) {
|
||||
Fuzzer::FuzzerResultStatus Fuzzer::Run(
|
||||
const std::vector<uint32_t>& binary_in,
|
||||
const protobufs::FactSequence& initial_facts,
|
||||
std::vector<uint32_t>* binary_out,
|
||||
protobufs::TransformationSequence* transformation_sequence_out,
|
||||
spv_const_fuzzer_options options) const {
|
||||
spv_const_fuzzer_options options, 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);
|
||||
tools.SetMessageConsumer(impl_->consumer);
|
||||
if (!tools.IsValid()) {
|
||||
impl_->consumer(SPV_MSG_ERROR, nullptr, {},
|
||||
"Failed to create SPIRV-Tools interface; stopping.");
|
||||
|
@ -58,9 +58,8 @@ class Fuzzer {
|
||||
FuzzerResultStatus Run(
|
||||
const std::vector<uint32_t>& binary_in,
|
||||
const protobufs::FactSequence& initial_facts,
|
||||
std::vector<uint32_t>* binary_out,
|
||||
protobufs::TransformationSequence* transformation_sequence_out,
|
||||
spv_const_fuzzer_options options) const;
|
||||
spv_const_fuzzer_options options, std::vector<uint32_t>* binary_out,
|
||||
protobufs::TransformationSequence* transformation_sequence_out) const;
|
||||
|
||||
private:
|
||||
struct Impl; // Opaque struct for holding internal data.
|
||||
|
240
source/fuzz/shrinker.cpp
Normal file
240
source/fuzz/shrinker.cpp
Normal file
@ -0,0 +1,240 @@
|
||||
// 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,
|
||||
¤t_best_binary, ¤t_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
|
91
source/fuzz/shrinker.h
Normal file
91
source/fuzz/shrinker.h
Normal file
@ -0,0 +1,91 @@
|
||||
// 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.
|
||||
|
||||
#ifndef SOURCE_FUZZ_SHRINKER_H_
|
||||
#define SOURCE_FUZZ_SHRINKER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
|
||||
#include "spirv-tools/libspirv.hpp"
|
||||
|
||||
namespace spvtools {
|
||||
namespace fuzz {
|
||||
|
||||
// Shrinks a sequence of transformations that lead to an interesting SPIR-V
|
||||
// binary to yield a smaller sequence of transformations that still produce an
|
||||
// interesting binary.
|
||||
class Shrinker {
|
||||
public:
|
||||
// Possible statuses that can result from running the shrinker.
|
||||
enum ShrinkerResultStatus {
|
||||
kComplete,
|
||||
kFailedToCreateSpirvToolsInterface,
|
||||
kInitialBinaryInvalid,
|
||||
kInitialBinaryNotInteresting,
|
||||
kReplayFailed,
|
||||
kStepLimitReached,
|
||||
};
|
||||
|
||||
// The type for a function that will take a binary, |binary|, and return true
|
||||
// if and only if the binary is deemed interesting. (The function also takes
|
||||
// an integer argument, |counter|, that will be incremented each time the
|
||||
// function is called; this is for debugging purposes).
|
||||
//
|
||||
// The notion of "interesting" depends on what properties of the binary or
|
||||
// tools that process the binary we are trying to maintain during shrinking.
|
||||
using InterestingnessFunction = std::function<bool(
|
||||
const std::vector<uint32_t>& binary, uint32_t counter)>;
|
||||
|
||||
// Constructs a shrinker from the given target environment.
|
||||
Shrinker(spv_target_env env, uint32_t step_limit);
|
||||
|
||||
// Disables copy/move constructor/assignment operations.
|
||||
Shrinker(const Shrinker&) = delete;
|
||||
Shrinker(Shrinker&&) = delete;
|
||||
Shrinker& operator=(const Shrinker&) = delete;
|
||||
Shrinker& operator=(Shrinker&&) = delete;
|
||||
|
||||
~Shrinker();
|
||||
|
||||
// Sets the message consumer to the given |consumer|. The |consumer| will be
|
||||
// invoked once for each message communicated from the library.
|
||||
void SetMessageConsumer(MessageConsumer consumer);
|
||||
|
||||
// Requires that when |transformation_sequence_in| is applied to |binary_in|
|
||||
// with initial facts |initial_facts|, the resulting binary is interesting
|
||||
// according to |interestingness_function|.
|
||||
//
|
||||
// Produces, via |transformation_sequence_out|, a subsequence of
|
||||
// |transformation_sequence_in| that, when applied with initial facts
|
||||
// |initial_facts|, produces a binary (captured via |binary_out|) that is
|
||||
// also interesting according to |interestingness_function|.
|
||||
ShrinkerResultStatus Run(
|
||||
const std::vector<uint32_t>& binary_in,
|
||||
const protobufs::FactSequence& initial_facts,
|
||||
const protobufs::TransformationSequence& transformation_sequence_in,
|
||||
const InterestingnessFunction& interestingness_function,
|
||||
std::vector<uint32_t>* binary_out,
|
||||
protobufs::TransformationSequence* transformation_sequence_out) const;
|
||||
|
||||
private:
|
||||
struct Impl; // Opaque struct for holding internal data.
|
||||
std::unique_ptr<Impl> impl_; // Unique pointer to internal data.
|
||||
};
|
||||
|
||||
} // namespace fuzz
|
||||
} // namespace spvtools
|
||||
|
||||
#endif // SOURCE_FUZZ_SHRINKER_H_
|
@ -14,7 +14,15 @@
|
||||
|
||||
#include "source/spirv_fuzzer_options.h"
|
||||
|
||||
spv_fuzzer_options_t::spv_fuzzer_options_t() = default;
|
||||
namespace {
|
||||
// The default maximum number of steps for the reducer to run before giving up.
|
||||
const uint32_t kDefaultStepLimit = 250;
|
||||
} // namespace
|
||||
|
||||
spv_fuzzer_options_t::spv_fuzzer_options_t()
|
||||
: has_random_seed(false),
|
||||
random_seed(0),
|
||||
shrinker_step_limit(kDefaultStepLimit) {}
|
||||
|
||||
SPIRV_TOOLS_EXPORT spv_fuzzer_options spvFuzzerOptionsCreate() {
|
||||
return new spv_fuzzer_options_t();
|
||||
@ -29,3 +37,8 @@ SPIRV_TOOLS_EXPORT void spvFuzzerOptionsSetRandomSeed(
|
||||
options->has_random_seed = true;
|
||||
options->random_seed = seed;
|
||||
}
|
||||
|
||||
SPIRV_TOOLS_EXPORT void spvFuzzerOptionsSetShrinkerStepLimit(
|
||||
spv_fuzzer_options options, uint32_t shrinker_step_limit) {
|
||||
options->shrinker_step_limit = shrinker_step_limit;
|
||||
}
|
||||
|
@ -26,8 +26,11 @@ struct spv_fuzzer_options_t {
|
||||
spv_fuzzer_options_t();
|
||||
|
||||
// See spvFuzzerOptionsSetRandomSeed.
|
||||
bool has_random_seed = false;
|
||||
uint32_t random_seed = 0;
|
||||
bool has_random_seed;
|
||||
uint32_t random_seed;
|
||||
|
||||
// See spvFuzzerOptionsSetShrinkerStepLimit.
|
||||
uint32_t shrinker_step_limit;
|
||||
};
|
||||
|
||||
#endif // SOURCE_SPIRV_FUZZER_OPTIONS_H_
|
||||
|
@ -18,6 +18,7 @@ if (${SPIRV_BUILD_FUZZER})
|
||||
fuzz_test_util.h
|
||||
|
||||
fuzzer_replayer_test.cpp
|
||||
fuzzer_shrinker_test.cpp
|
||||
fact_manager_test.cpp
|
||||
fuzz_test_util.cpp
|
||||
fuzzer_pass_add_useful_constructs_test.cpp
|
||||
|
@ -59,6 +59,11 @@ const uint32_t kFuzzAssembleOption =
|
||||
const uint32_t kFuzzDisassembleOption =
|
||||
SPV_BINARY_TO_TEXT_OPTION_NO_HEADER | SPV_BINARY_TO_TEXT_OPTION_INDENT;
|
||||
|
||||
// A silent message consumer.
|
||||
const spvtools::MessageConsumer kSilentConsumer =
|
||||
[](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char*) -> void {};
|
||||
|
||||
} // namespace fuzz
|
||||
} // namespace spvtools
|
||||
|
||||
|
@ -43,9 +43,10 @@ void RunFuzzerAndReplayer(const std::string& shader,
|
||||
spvFuzzerOptionsSetRandomSeed(fuzzer_options, seed);
|
||||
|
||||
Fuzzer fuzzer(env);
|
||||
fuzzer.SetMessageConsumer(kSilentConsumer);
|
||||
auto fuzzer_result_status =
|
||||
fuzzer.Run(binary_in, initial_facts, &fuzzer_binary_out,
|
||||
&fuzzer_transformation_sequence_out, fuzzer_options);
|
||||
fuzzer.Run(binary_in, initial_facts, fuzzer_options, &fuzzer_binary_out,
|
||||
&fuzzer_transformation_sequence_out);
|
||||
ASSERT_EQ(Fuzzer::FuzzerResultStatus::kComplete, fuzzer_result_status);
|
||||
ASSERT_TRUE(t.Validate(fuzzer_binary_out));
|
||||
|
||||
@ -53,6 +54,7 @@ void RunFuzzerAndReplayer(const std::string& shader,
|
||||
protobufs::TransformationSequence replayer_transformation_sequence_out;
|
||||
|
||||
Replayer replayer(env);
|
||||
replayer.SetMessageConsumer(kSilentConsumer);
|
||||
auto replayer_result_status = replayer.Run(
|
||||
binary_in, initial_facts, fuzzer_transformation_sequence_out,
|
||||
&replayer_binary_out, &replayer_transformation_sequence_out);
|
||||
|
1109
test/fuzz/fuzzer_shrinker_test.cpp
Normal file
1109
test/fuzz/fuzzer_shrinker_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,11 +17,13 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "source/fuzz/fuzzer.h"
|
||||
#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
|
||||
#include "source/fuzz/replayer.h"
|
||||
#include "source/fuzz/shrinker.h"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/log.h"
|
||||
@ -32,10 +34,30 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// Check that the std::system function can actually be used.
|
||||
bool CheckExecuteCommand() {
|
||||
int res = std::system(nullptr);
|
||||
return res != 0;
|
||||
}
|
||||
|
||||
// Execute a command using the shell.
|
||||
// Returns true if and only if the command's exit status was 0.
|
||||
bool ExecuteCommand(const std::string& command) {
|
||||
errno = 0;
|
||||
int status = std::system(command.c_str());
|
||||
assert(errno == 0 && "failed to execute command");
|
||||
// The result returned by 'system' is implementation-defined, but is
|
||||
// usually the case that the returned value is 0 when the command's exit
|
||||
// code was 0. We are assuming that here, and that's all we depend on.
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
// Status and actions to perform after parsing command-line arguments.
|
||||
enum class FuzzActions {
|
||||
FUZZ, // Run the fuzzer to apply transformations in a randomized fashion.
|
||||
REPLAY, // Replay an existing sequence of transformations.
|
||||
SHRINK, // Shrink an existing sequence of transformations with respect to an
|
||||
// interestingness function.
|
||||
STOP // Do nothing.
|
||||
};
|
||||
|
||||
@ -71,6 +93,17 @@ Options (in lexicographical order):
|
||||
--seed
|
||||
Unsigned 32-bit integer seed to control random number
|
||||
generation.
|
||||
--shrink
|
||||
File from which to read a sequence of transformations to shrink
|
||||
(instead of fuzzing)
|
||||
--shrinker-step-limit
|
||||
Unsigned 32-bit integer specifying maximum number of steps the
|
||||
shrinker will take before giving up. Ignored unless --shrink
|
||||
is used.
|
||||
--interestingness
|
||||
Path to an interestingness function to guide shrinking: a script
|
||||
that returns 0 if and only if a given binary is interesting.
|
||||
Required if --shrink is provided; disallowed otherwise.
|
||||
--version
|
||||
Display fuzzer version information.
|
||||
|
||||
@ -99,6 +132,8 @@ bool EndsWithSpv(const std::string& filename) {
|
||||
FuzzStatus ParseFlags(int argc, const char** argv, std::string* in_binary_file,
|
||||
std::string* out_binary_file,
|
||||
std::string* replay_transformations_file,
|
||||
std::string* interestingness_function_file,
|
||||
std::string* shrink_transformations_file,
|
||||
spvtools::FuzzerOptions* fuzzer_options) {
|
||||
uint32_t positional_arg_index = 0;
|
||||
|
||||
@ -122,6 +157,13 @@ FuzzStatus ParseFlags(int argc, const char** argv, std::string* in_binary_file,
|
||||
} else if (0 == strncmp(cur_arg, "--replay=", sizeof("--replay=") - 1)) {
|
||||
const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
|
||||
*replay_transformations_file = std::string(split_flag.second);
|
||||
} else if (0 == strncmp(cur_arg, "--interestingness=",
|
||||
sizeof("--interestingness=") - 1)) {
|
||||
const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
|
||||
*interestingness_function_file = std::string(split_flag.second);
|
||||
} else if (0 == strncmp(cur_arg, "--shrink=", sizeof("--shrink=") - 1)) {
|
||||
const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
|
||||
*shrink_transformations_file = std::string(split_flag.second);
|
||||
} else if (0 == strncmp(cur_arg, "--seed=", sizeof("--seed=") - 1)) {
|
||||
const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
|
||||
char* end = nullptr;
|
||||
@ -130,6 +172,15 @@ FuzzStatus ParseFlags(int argc, const char** argv, std::string* in_binary_file,
|
||||
static_cast<uint32_t>(strtol(split_flag.second.c_str(), &end, 10));
|
||||
assert(end != split_flag.second.c_str() && errno == 0);
|
||||
fuzzer_options->set_random_seed(seed);
|
||||
} else if (0 == strncmp(cur_arg, "--shrinker-step-limit=",
|
||||
sizeof("--shrinker-step-limit=") - 1)) {
|
||||
const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
|
||||
char* end = nullptr;
|
||||
errno = 0;
|
||||
const auto step_limit =
|
||||
static_cast<uint32_t>(strtol(split_flag.second.c_str(), &end, 10));
|
||||
assert(end != split_flag.second.c_str() && errno == 0);
|
||||
fuzzer_options->set_shrinker_step_limit(step_limit);
|
||||
} else if ('\0' == cur_arg[1]) {
|
||||
// We do not support fuzzing from standard input. We could support
|
||||
// this if there was a compelling use case.
|
||||
@ -173,12 +224,59 @@ FuzzStatus ParseFlags(int argc, const char** argv, std::string* in_binary_file,
|
||||
if (!replay_transformations_file->empty()) {
|
||||
// A replay transformations file was given, thus the tool is being invoked
|
||||
// in replay mode.
|
||||
if (!shrink_transformations_file->empty()) {
|
||||
spvtools::Error(
|
||||
FuzzDiagnostic, nullptr, {},
|
||||
"The --replay and --shrink arguments are mutually exclusive.");
|
||||
return {FuzzActions::STOP, 1};
|
||||
}
|
||||
if (!interestingness_function_file->empty()) {
|
||||
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
||||
"The --replay and --interestingness arguments are "
|
||||
"mutually exclusive.");
|
||||
return {FuzzActions::STOP, 1};
|
||||
}
|
||||
return {FuzzActions::REPLAY, 0};
|
||||
}
|
||||
|
||||
if (!shrink_transformations_file->empty() ^
|
||||
!interestingness_function_file->empty()) {
|
||||
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
||||
"Both or neither of the --shrink and --interestingness "
|
||||
"arguments must be provided.");
|
||||
return {FuzzActions::STOP, 1};
|
||||
}
|
||||
|
||||
if (!shrink_transformations_file->empty()) {
|
||||
// The tool is being invoked in shrink mode.
|
||||
assert(!interestingness_function_file->empty() &&
|
||||
"An error should have been raised if --shrink but not --interesting "
|
||||
"was provided.");
|
||||
return {FuzzActions::SHRINK, 0};
|
||||
}
|
||||
|
||||
return {FuzzActions::FUZZ, 0};
|
||||
}
|
||||
|
||||
bool ParseTransformations(
|
||||
const std::string& transformations_file,
|
||||
spvtools::fuzz::protobufs::TransformationSequence* transformations) {
|
||||
std::ifstream transformations_stream;
|
||||
transformations_stream.open(transformations_file,
|
||||
std::ios::in | std::ios::binary);
|
||||
auto parse_success =
|
||||
transformations->ParseFromIstream(&transformations_stream);
|
||||
transformations_stream.close();
|
||||
if (!parse_success) {
|
||||
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
||||
("Error reading transformations from file '" +
|
||||
transformations_file + "'")
|
||||
.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Replay(const spv_target_env& target_env,
|
||||
const std::vector<uint32_t>& binary_in,
|
||||
const spvtools::fuzz::protobufs::FactSequence& initial_facts,
|
||||
@ -186,29 +284,61 @@ bool Replay(const spv_target_env& target_env,
|
||||
std::vector<uint32_t>* binary_out,
|
||||
spvtools::fuzz::protobufs::TransformationSequence*
|
||||
transformations_applied) {
|
||||
std::ifstream existing_transformations_file;
|
||||
existing_transformations_file.open(replay_transformations_file,
|
||||
std::ios::in | std::ios::binary);
|
||||
spvtools::fuzz::protobufs::TransformationSequence
|
||||
existing_transformation_sequence;
|
||||
auto parse_success = existing_transformation_sequence.ParseFromIstream(
|
||||
&existing_transformations_file);
|
||||
existing_transformations_file.close();
|
||||
if (!parse_success) {
|
||||
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
||||
"Error reading transformations for replay");
|
||||
spvtools::fuzz::protobufs::TransformationSequence transformation_sequence;
|
||||
if (!ParseTransformations(replay_transformations_file,
|
||||
&transformation_sequence)) {
|
||||
return false;
|
||||
}
|
||||
spvtools::fuzz::Replayer replayer(target_env);
|
||||
replayer.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
|
||||
auto replay_result_status =
|
||||
replayer.Run(binary_in, initial_facts, existing_transformation_sequence,
|
||||
replayer.Run(binary_in, initial_facts, transformation_sequence,
|
||||
binary_out, transformations_applied);
|
||||
if (replay_result_status !=
|
||||
spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete) {
|
||||
return !(replay_result_status !=
|
||||
spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete);
|
||||
}
|
||||
|
||||
bool Shrink(const spv_target_env& target_env,
|
||||
spv_const_fuzzer_options fuzzer_options,
|
||||
const std::vector<uint32_t>& binary_in,
|
||||
const spvtools::fuzz::protobufs::FactSequence& initial_facts,
|
||||
const std::string& shrink_transformations_file,
|
||||
const std::string& interestingness_function_file,
|
||||
std::vector<uint32_t>* binary_out,
|
||||
spvtools::fuzz::protobufs::TransformationSequence*
|
||||
transformations_applied) {
|
||||
spvtools::fuzz::protobufs::TransformationSequence transformation_sequence;
|
||||
if (!ParseTransformations(shrink_transformations_file,
|
||||
&transformation_sequence)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
spvtools::fuzz::Shrinker shrinker(target_env,
|
||||
fuzzer_options->shrinker_step_limit);
|
||||
shrinker.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
|
||||
|
||||
spvtools::fuzz::Shrinker::InterestingnessFunction interestingness_function =
|
||||
[interestingness_function_file](std::vector<uint32_t> binary,
|
||||
uint32_t reductions_applied) -> bool {
|
||||
std::stringstream ss;
|
||||
ss << "temp_" << std::setw(4) << std::setfill('0') << reductions_applied
|
||||
<< ".spv";
|
||||
const auto spv_file = ss.str();
|
||||
const std::string command =
|
||||
std::string(interestingness_function_file) + " " + spv_file;
|
||||
auto write_file_succeeded =
|
||||
WriteFile(spv_file.c_str(), "wb", &binary[0], binary.size());
|
||||
(void)(write_file_succeeded);
|
||||
assert(write_file_succeeded);
|
||||
return ExecuteCommand(command);
|
||||
};
|
||||
|
||||
auto shrink_result_status = shrinker.Run(
|
||||
binary_in, initial_facts, transformation_sequence,
|
||||
interestingness_function, binary_out, transformations_applied);
|
||||
return spvtools::fuzz::Shrinker::ShrinkerResultStatus::kComplete ==
|
||||
shrink_result_status ||
|
||||
spvtools::fuzz::Shrinker::ShrinkerResultStatus::kStepLimitReached ==
|
||||
shrink_result_status;
|
||||
}
|
||||
|
||||
bool Fuzz(const spv_target_env& target_env,
|
||||
@ -220,8 +350,8 @@ bool Fuzz(const spv_target_env& target_env,
|
||||
transformations_applied) {
|
||||
spvtools::fuzz::Fuzzer fuzzer(target_env);
|
||||
fuzzer.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
|
||||
auto fuzz_result_status = fuzzer.Run(binary_in, initial_facts, binary_out,
|
||||
transformations_applied, fuzzer_options);
|
||||
auto fuzz_result_status = fuzzer.Run(binary_in, initial_facts, fuzzer_options,
|
||||
binary_out, transformations_applied);
|
||||
if (fuzz_result_status !=
|
||||
spvtools::fuzz::Fuzzer::FuzzerResultStatus::kComplete) {
|
||||
spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error running fuzzer");
|
||||
@ -238,11 +368,15 @@ int main(int argc, const char** argv) {
|
||||
std::string in_binary_file;
|
||||
std::string out_binary_file;
|
||||
std::string replay_transformations_file;
|
||||
std::string interestingness_function_file;
|
||||
std::string shrink_transformations_file;
|
||||
|
||||
spvtools::FuzzerOptions fuzzer_options;
|
||||
|
||||
FuzzStatus status = ParseFlags(argc, argv, &in_binary_file, &out_binary_file,
|
||||
&replay_transformations_file, &fuzzer_options);
|
||||
FuzzStatus status =
|
||||
ParseFlags(argc, argv, &in_binary_file, &out_binary_file,
|
||||
&replay_transformations_file, &interestingness_function_file,
|
||||
&shrink_transformations_file, &fuzzer_options);
|
||||
|
||||
if (status.action == FuzzActions::STOP) {
|
||||
return status.code;
|
||||
@ -290,6 +424,18 @@ int main(int argc, const char** argv) {
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case FuzzActions::SHRINK: {
|
||||
if (!CheckExecuteCommand()) {
|
||||
std::cerr << "could not find shell interpreter for executing a command"
|
||||
<< std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (!Shrink(target_env, fuzzer_options, binary_in, initial_facts,
|
||||
shrink_transformations_file, interestingness_function_file,
|
||||
&binary_out, &transformations_applied)) {
|
||||
return 1;
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
assert(false && "Unknown fuzzer action.");
|
||||
break;
|
||||
|
Loading…
Reference in New Issue
Block a user