mirror of
https://github.com/KhronosGroup/SPIRV-Tools
synced 2024-12-27 02:10:15 +00:00
482 lines
18 KiB
C++
482 lines
18 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 <cassert>
|
|
#include <cerrno>
|
|
#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"
|
|
#include "source/spirv_fuzzer_options.h"
|
|
#include "source/util/string_utils.h"
|
|
#include "tools/io.h"
|
|
#include "tools/util/cli_consumer.h"
|
|
|
|
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.
|
|
};
|
|
|
|
struct FuzzStatus {
|
|
FuzzActions action;
|
|
int code;
|
|
};
|
|
|
|
void PrintUsage(const char* program) {
|
|
// NOTE: Please maintain flags in lexicographical order.
|
|
printf(
|
|
R"(%s - Fuzzes an equivalent SPIR-V binary based on a given binary.
|
|
|
|
USAGE: %s [options] <input.spv> -o <output.spv>
|
|
|
|
The SPIR-V binary is read from <input.spv>, which must have extension .spv. If
|
|
<input.facts> is also present, facts about the SPIR-V binary are read from this
|
|
file.
|
|
|
|
The transformed SPIR-V binary is written to <output.spv>. Human-readable and
|
|
binary representations of the transformations that were applied are written to
|
|
<output.transformations_json> and <output.transformations>, respectively.
|
|
|
|
NOTE: The fuzzer is a work in progress.
|
|
|
|
Options (in lexicographical order):
|
|
|
|
-h, --help
|
|
Print this help.
|
|
--replay
|
|
File from which to read a sequence of transformations to replay
|
|
(instead of fuzzing)
|
|
--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.
|
|
|
|
)",
|
|
program, program);
|
|
}
|
|
|
|
// Message consumer for this tool. Used to emit diagnostics during
|
|
// initialization and setup. Note that |source| and |position| are irrelevant
|
|
// here because we are still not processing a SPIR-V input file.
|
|
void FuzzDiagnostic(spv_message_level_t level, const char* /*source*/,
|
|
const spv_position_t& /*position*/, const char* message) {
|
|
if (level == SPV_MSG_ERROR) {
|
|
fprintf(stderr, "error: ");
|
|
}
|
|
fprintf(stderr, "%s\n", message);
|
|
}
|
|
|
|
bool EndsWithSpv(const std::string& filename) {
|
|
std::string dot_spv = ".spv";
|
|
return filename.length() >= dot_spv.length() &&
|
|
0 == filename.compare(filename.length() - dot_spv.length(),
|
|
filename.length(), dot_spv);
|
|
}
|
|
|
|
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;
|
|
|
|
for (int argi = 1; argi < argc; ++argi) {
|
|
const char* cur_arg = argv[argi];
|
|
if ('-' == cur_arg[0]) {
|
|
if (0 == strcmp(cur_arg, "--version")) {
|
|
spvtools::Logf(FuzzDiagnostic, SPV_MSG_INFO, nullptr, {}, "%s\n",
|
|
spvSoftwareVersionDetailsString());
|
|
return {FuzzActions::STOP, 0};
|
|
} else if (0 == strcmp(cur_arg, "--help") || 0 == strcmp(cur_arg, "-h")) {
|
|
PrintUsage(argv[0]);
|
|
return {FuzzActions::STOP, 0};
|
|
} else if (0 == strcmp(cur_arg, "-o")) {
|
|
if (out_binary_file->empty() && argi + 1 < argc) {
|
|
*out_binary_file = std::string(argv[++argi]);
|
|
} else {
|
|
PrintUsage(argv[0]);
|
|
return {FuzzActions::STOP, 1};
|
|
}
|
|
} 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;
|
|
errno = 0;
|
|
const auto seed =
|
|
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.
|
|
PrintUsage(argv[0]);
|
|
return {FuzzActions::STOP, 0};
|
|
}
|
|
} else if (positional_arg_index == 0) {
|
|
// Binary input file name
|
|
assert(in_binary_file->empty());
|
|
*in_binary_file = std::string(cur_arg);
|
|
positional_arg_index++;
|
|
} else {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
|
"Too many positional arguments specified");
|
|
return {FuzzActions::STOP, 1};
|
|
}
|
|
}
|
|
|
|
if (in_binary_file->empty()) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {}, "No input file specified");
|
|
return {FuzzActions::STOP, 1};
|
|
}
|
|
|
|
if (!EndsWithSpv(*in_binary_file)) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
|
"Input filename must have extension .spv");
|
|
return {FuzzActions::STOP, 1};
|
|
}
|
|
|
|
if (out_binary_file->empty()) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {}, "-o required");
|
|
return {FuzzActions::STOP, 1};
|
|
}
|
|
|
|
if (!EndsWithSpv(*out_binary_file)) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
|
"Output filename must have extension .spv");
|
|
return {FuzzActions::STOP, 1};
|
|
}
|
|
|
|
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,
|
|
const std::string& replay_transformations_file,
|
|
std::vector<uint32_t>* binary_out,
|
|
spvtools::fuzz::protobufs::TransformationSequence*
|
|
transformations_applied) {
|
|
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, transformation_sequence,
|
|
binary_out, transformations_applied);
|
|
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;
|
|
}
|
|
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,
|
|
const spvtools::FuzzerOptions& fuzzer_options,
|
|
const std::vector<uint32_t>& binary_in,
|
|
const spvtools::fuzz::protobufs::FactSequence& initial_facts,
|
|
std::vector<uint32_t>* binary_out,
|
|
spvtools::fuzz::protobufs::TransformationSequence*
|
|
transformations_applied) {
|
|
spvtools::fuzz::Fuzzer fuzzer(target_env);
|
|
fuzzer.SetMessageConsumer(spvtools::utils::CLIMessageConsumer);
|
|
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");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
|
|
|
|
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, &interestingness_function_file,
|
|
&shrink_transformations_file, &fuzzer_options);
|
|
|
|
if (status.action == FuzzActions::STOP) {
|
|
return status.code;
|
|
}
|
|
|
|
std::vector<uint32_t> binary_in;
|
|
if (!ReadFile<uint32_t>(in_binary_file.c_str(), "rb", &binary_in)) {
|
|
return 1;
|
|
}
|
|
|
|
spvtools::fuzz::protobufs::FactSequence initial_facts;
|
|
const std::string dot_spv(".spv");
|
|
std::string in_facts_file =
|
|
in_binary_file.substr(0, in_binary_file.length() - dot_spv.length()) +
|
|
".facts";
|
|
std::ifstream facts_input(in_facts_file);
|
|
if (facts_input) {
|
|
std::string facts_json_string((std::istreambuf_iterator<char>(facts_input)),
|
|
std::istreambuf_iterator<char>());
|
|
facts_input.close();
|
|
if (google::protobuf::util::Status::OK !=
|
|
google::protobuf::util::JsonStringToMessage(facts_json_string,
|
|
&initial_facts)) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error reading facts data");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
std::vector<uint32_t> binary_out;
|
|
spvtools::fuzz::protobufs::TransformationSequence transformations_applied;
|
|
|
|
spv_target_env target_env = kDefaultEnvironment;
|
|
|
|
switch (status.action) {
|
|
case FuzzActions::FUZZ:
|
|
if (!Fuzz(target_env, fuzzer_options, binary_in, initial_facts,
|
|
&binary_out, &transformations_applied)) {
|
|
return 1;
|
|
}
|
|
break;
|
|
case FuzzActions::REPLAY:
|
|
if (!Replay(target_env, binary_in, initial_facts,
|
|
replay_transformations_file, &binary_out,
|
|
&transformations_applied)) {
|
|
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;
|
|
}
|
|
|
|
if (!WriteFile<uint32_t>(out_binary_file.c_str(), "wb", binary_out.data(),
|
|
binary_out.size())) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error writing out binary");
|
|
return 1;
|
|
}
|
|
|
|
std::string output_file_prefix =
|
|
out_binary_file.substr(0, out_binary_file.length() - dot_spv.length());
|
|
std::ofstream transformations_file;
|
|
transformations_file.open(output_file_prefix + ".transformations",
|
|
std::ios::out | std::ios::binary);
|
|
bool success =
|
|
transformations_applied.SerializeToOstream(&transformations_file);
|
|
transformations_file.close();
|
|
if (!success) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
|
"Error writing out transformations binary");
|
|
return 1;
|
|
}
|
|
|
|
std::string json_string;
|
|
auto json_options = google::protobuf::util::JsonOptions();
|
|
json_options.add_whitespace = true;
|
|
auto json_generation_status = google::protobuf::util::MessageToJsonString(
|
|
transformations_applied, &json_string, json_options);
|
|
if (json_generation_status != google::protobuf::util::Status::OK) {
|
|
spvtools::Error(FuzzDiagnostic, nullptr, {},
|
|
"Error writing out transformations in JSON format");
|
|
return 1;
|
|
}
|
|
|
|
std::ofstream transformations_json_file(output_file_prefix +
|
|
".transformations_json");
|
|
transformations_json_file << json_string;
|
|
transformations_json_file.close();
|
|
|
|
return 0;
|
|
}
|