| // 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 <memory> |
| #include <random> |
| #include <sstream> |
| #include <string> |
| |
| #include "source/fuzz/force_render_red.h" |
| #include "source/fuzz/fuzzer.h" |
| #include "source/fuzz/fuzzer_util.h" |
| #include "source/fuzz/protobufs/spirvfuzz_protobufs.h" |
| #include "source/fuzz/pseudo_random_generator.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/make_unique.h" |
| #include "source/util/string_utils.h" |
| #include "tools/io.h" |
| #include "tools/util/cli_consumer.h" |
| |
| namespace { |
| |
| enum class FuzzingTarget { kSpirv, kWgsl }; |
| |
| // 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 { |
| FORCE_RENDER_RED, // Turn the shader into a form such that it is guaranteed |
| // to render a red image. |
| 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> \ |
| --donors=<donors.txt> |
| USAGE: %s [options] <input.spv> -o <output.spv> \ |
| --shrink=<input.transformations> -- <interestingness_test> [args...] |
| |
| The SPIR-V binary is read from <input.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. |
| |
| When passing --shrink=<input.transformations> an <interestingness_test> |
| must also be provided; this is the path to a script that returns 0 if and only |
| if a given SPIR-V binary is interesting. The SPIR-V binary will be passed to |
| the script as an argument after any other provided arguments [args...]. The |
| "--" characters are optional but denote that all arguments that follow are |
| positional arguments and thus will be forwarded to the interestingness script, |
| and not parsed by %s. |
| |
| NOTE: The fuzzer is a work in progress. |
| |
| Options (in lexicographical order): |
| |
| -h, --help |
| Print this help. |
| --donors= |
| File specifying a series of donor files, one per line. Must be |
| provided if the tool is invoked in fuzzing mode; incompatible |
| with replay and shrink modes. The file should be empty if no |
| donors are to be used. |
| --enable-all-passes |
| By default, spirv-fuzz follows the philosophy of "swarm testing" |
| (Groce et al., 2012): only a subset of fuzzer passes are enabled |
| on any given fuzzer run, with the subset being chosen randomly. |
| This flag instead forces *all* fuzzer passes to be enabled. When |
| running spirv-fuzz many times this is likely to produce *less* |
| diverse fuzzed modules than when swarm testing is used. The |
| purpose of the flag is to allow that hypothesis to be tested. |
| --force-render-red |
| Transforms the input shader into a shader that writes red to the |
| output buffer, and then captures the original shader as the body |
| of a conditional with a dynamically false guard. Exploits input |
| facts to make the guard non-obviously false. This option is a |
| helper for massaging crash-inducing tests into a runnable |
| format; it does not perform any fuzzing. |
| --fuzzer-pass-validation |
| Run the validator after applying each fuzzer pass during |
| fuzzing. Aborts fuzzing early if an invalid binary is created. |
| Useful for debugging spirv-fuzz. |
| --repeated-pass-strategy= |
| Available strategies are: |
| - looped (the default): a sequence of fuzzer passes is chosen at |
| the start of fuzzing, via randomly choosing enabled passes, and |
| augmenting these choices with fuzzer passes that it is |
| recommended to run subsequently. Fuzzing then involves |
| repeatedly applying this fixed sequence of passes. |
| - random: each time a fuzzer pass is requested, this strategy |
| either provides one at random from the set of enabled passes, |
| or provides a pass that has been recommended based on a pass |
| that was used previously. |
| - simple: each time a fuzzer pass is requested, one is provided |
| at random from the set of enabled passes. |
| --fuzzing-target= |
| This option will adjust probabilities of applying certain |
| transformations s.t. the module always remains valid according |
| to the semantics of some fuzzing target. Available targets: |
| - spir-v - module is valid according to the SPIR-V spec. |
| - wgsl - module is valid according to the WGSL spec. |
| --replay |
| File from which to read a sequence of transformations to replay |
| (instead of fuzzing) |
| --replay-range= |
| Signed 32-bit integer. If set to a positive value N, only the |
| first N transformations will be applied during replay. If set to |
| a negative value -N, all but the final N transformations will be |
| applied during replay. If set to 0 (the default), all |
| transformations will be applied during replay. Ignored unless |
| --replay is used. |
| --replay-validation |
| Run the validator after applying each transformation during |
| replay (including the replay that occurs during shrinking). |
| Aborts if an invalid binary is created. Useful for debugging |
| spirv-fuzz. |
| --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. |
| --shrinker-temp-file-prefix= |
| Specifies a temporary file prefix that will be used to output |
| temporary shader files during shrinking. A number and .spv |
| extension will be added. The default is "temp_", which will |
| cause files like "temp_0001.spv" to be output to the current |
| directory. Ignored unless --shrink is used. |
| --version |
| Display fuzzer version information. |
| |
| Supported validator options are as follows. See `spirv-val --help` for details. |
| --before-hlsl-legalization |
| --relax-block-layout |
| --relax-logical-pointer |
| --relax-struct-store |
| --scalar-block-layout |
| --skip-block-layout |
| )", |
| program, program, 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); |
| } |
| |
| FuzzStatus ParseFlags( |
| int argc, const char** argv, std::string* in_binary_file, |
| std::string* out_binary_file, std::string* donors_file, |
| std::string* replay_transformations_file, |
| std::vector<std::string>* interestingness_test, |
| std::string* shrink_transformations_file, |
| std::string* shrink_temp_file_prefix, |
| spvtools::fuzz::RepeatedPassStrategy* repeated_pass_strategy, |
| FuzzingTarget* fuzzing_target, spvtools::FuzzerOptions* fuzzer_options, |
| spvtools::ValidatorOptions* validator_options) { |
| uint32_t positional_arg_index = 0; |
| bool only_positional_arguments_remain = false; |
| bool force_render_red = false; |
| |
| *repeated_pass_strategy = |
| spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations; |
| |
| for (int argi = 1; argi < argc; ++argi) { |
| const char* cur_arg = argv[argi]; |
| if ('-' == cur_arg[0] && !only_positional_arguments_remain) { |
| 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, "--donors=", sizeof("--donors=") - 1)) { |
| const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg); |
| *donors_file = std::string(split_flag.second); |
| } else if (0 == strncmp(cur_arg, "--enable-all-passes", |
| sizeof("--enable-all-passes") - 1)) { |
| fuzzer_options->enable_all_passes(); |
| } else if (0 == strncmp(cur_arg, "--force-render-red", |
| sizeof("--force-render-red") - 1)) { |
| force_render_red = true; |
| } else if (0 == strncmp(cur_arg, "--fuzzer-pass-validation", |
| sizeof("--fuzzer-pass-validation") - 1)) { |
| fuzzer_options->enable_fuzzer_pass_validation(); |
| } 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, "--repeated-pass-strategy=", |
| sizeof("--repeated-pass-strategy=") - 1)) { |
| std::string strategy = spvtools::utils::SplitFlagArgs(cur_arg).second; |
| if (strategy == "looped") { |
| *repeated_pass_strategy = |
| spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations; |
| } else if (strategy == "random") { |
| *repeated_pass_strategy = |
| spvtools::fuzz::RepeatedPassStrategy::kRandomWithRecommendations; |
| } else if (strategy == "simple") { |
| *repeated_pass_strategy = |
| spvtools::fuzz::RepeatedPassStrategy::kSimple; |
| } else { |
| std::stringstream ss; |
| ss << "Unknown repeated pass strategy '" << strategy << "'" |
| << std::endl; |
| ss << "Valid options are 'looped', 'random' and 'simple'."; |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, ss.str().c_str()); |
| return {FuzzActions::STOP, 1}; |
| } |
| } else if (0 == strncmp(cur_arg, "--fuzzing-target=", |
| sizeof("--fuzzing-target=") - 1)) { |
| std::string target = spvtools::utils::SplitFlagArgs(cur_arg).second; |
| if (target == "spir-v") { |
| *fuzzing_target = FuzzingTarget::kSpirv; |
| } else if (target == "wgsl") { |
| *fuzzing_target = FuzzingTarget::kWgsl; |
| } else { |
| std::stringstream ss; |
| ss << "Unknown fuzzing target '" << target << "'" << std::endl; |
| ss << "Valid options are 'spir-v' and 'wgsl'."; |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, ss.str().c_str()); |
| return {FuzzActions::STOP, 1}; |
| } |
| } else if (0 == strncmp(cur_arg, "--replay-range=", |
| sizeof("--replay-range=") - 1)) { |
| const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg); |
| char* end = nullptr; |
| errno = 0; |
| const auto replay_range = |
| static_cast<int32_t>(strtol(split_flag.second.c_str(), &end, 10)); |
| assert(end != split_flag.second.c_str() && errno == 0); |
| fuzzer_options->set_replay_range(replay_range); |
| } else if (0 == strncmp(cur_arg, "--replay-validation", |
| sizeof("--replay-validation") - 1)) { |
| fuzzer_options->enable_replay_validation(); |
| } 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 == strncmp(cur_arg, "--shrinker-temp-file-prefix=", |
| sizeof("--shrinker-temp-file-prefix=") - 1)) { |
| const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg); |
| *shrink_temp_file_prefix = std::string(split_flag.second); |
| } else if (0 == strcmp(cur_arg, "--before-hlsl-legalization")) { |
| validator_options->SetBeforeHlslLegalization(true); |
| } else if (0 == strcmp(cur_arg, "--relax-logical-pointer")) { |
| validator_options->SetRelaxLogicalPointer(true); |
| } else if (0 == strcmp(cur_arg, "--relax-block-layout")) { |
| validator_options->SetRelaxBlockLayout(true); |
| } else if (0 == strcmp(cur_arg, "--scalar-block-layout")) { |
| validator_options->SetScalarBlockLayout(true); |
| } else if (0 == strcmp(cur_arg, "--skip-block-layout")) { |
| validator_options->SetSkipBlockLayout(true); |
| } else if (0 == strcmp(cur_arg, "--relax-struct-store")) { |
| validator_options->SetRelaxStructStore(true); |
| } else if (0 == strcmp(cur_arg, "--")) { |
| only_positional_arguments_remain = true; |
| } else { |
| std::stringstream ss; |
| ss << "Unrecognized argument: " << cur_arg << std::endl; |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, ss.str().c_str()); |
| PrintUsage(argv[0]); |
| return {FuzzActions::STOP, 1}; |
| } |
| } 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 { |
| interestingness_test->push_back(std::string(cur_arg)); |
| } |
| } |
| |
| if (in_binary_file->empty()) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, "No input file specified"); |
| return {FuzzActions::STOP, 1}; |
| } |
| |
| if (out_binary_file->empty()) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, "-o required"); |
| return {FuzzActions::STOP, 1}; |
| } |
| |
| auto const_fuzzer_options = |
| static_cast<spv_const_fuzzer_options>(*fuzzer_options); |
| if (force_render_red) { |
| if (!replay_transformations_file->empty() || |
| !shrink_transformations_file->empty() || |
| const_fuzzer_options->replay_validation_enabled) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, |
| "The --force-render-red argument cannot be used with any " |
| "other arguments except -o."); |
| return {FuzzActions::STOP, 1}; |
| } |
| return {FuzzActions::FORCE_RENDER_RED, 0}; |
| } |
| |
| if (replay_transformations_file->empty() && |
| shrink_transformations_file->empty() && |
| static_cast<spv_const_fuzzer_options>(*fuzzer_options) |
| ->replay_validation_enabled) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, |
| "The --replay-validation argument can only be used with " |
| "one of the --replay or --shrink arguments."); |
| return {FuzzActions::STOP, 1}; |
| } |
| |
| if (shrink_transformations_file->empty() && !interestingness_test->empty()) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, |
| "Too many positional arguments specified; extra positional " |
| "arguments are used as the interestingness function, which " |
| "are only valid with the --shrink option."); |
| return {FuzzActions::STOP, 1}; |
| } |
| |
| if (!shrink_transformations_file->empty() && interestingness_test->empty()) { |
| spvtools::Error( |
| FuzzDiagnostic, nullptr, {}, |
| "The --shrink option requires an interestingness function."); |
| return {FuzzActions::STOP, 1}; |
| } |
| |
| if (!replay_transformations_file->empty() || |
| !shrink_transformations_file->empty()) { |
| // Donors should not be provided when replaying or shrinking: they only make |
| // sense during fuzzing. |
| if (!donors_file->empty()) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, |
| "The --donors argument is not compatible with --replay " |
| "nor --shrink."); |
| 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}; |
| } |
| return {FuzzActions::REPLAY, 0}; |
| } |
| |
| if (!shrink_transformations_file->empty()) { |
| // The tool is being invoked in shrink mode. |
| assert(!interestingness_test->empty() && |
| "An error should have been raised if --shrink was provided without " |
| "an interestingness test."); |
| return {FuzzActions::SHRINK, 0}; |
| } |
| |
| // The tool is being invoked in fuzz mode. |
| if (donors_file->empty()) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, |
| "Fuzzing requires that the --donors option is used."); |
| return {FuzzActions::STOP, 1}; |
| } |
| 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, |
| spv_const_fuzzer_options fuzzer_options, |
| spv_validator_options validator_options, |
| 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; |
| } |
| |
| uint32_t num_transformations_to_apply; |
| if (fuzzer_options->replay_range > 0) { |
| // We have a positive replay range, N. We would like transformations |
| // [0, N), truncated to the number of available transformations if N is too |
| // large. |
| num_transformations_to_apply = static_cast<uint32_t>( |
| std::min(fuzzer_options->replay_range, |
| transformation_sequence.transformation_size())); |
| } else { |
| // We have non-positive replay range, -N (where N may be 0). We would like |
| // transformations [0, num_transformations - N), or no transformations if N |
| // is too large. |
| num_transformations_to_apply = static_cast<uint32_t>( |
| std::max(0, transformation_sequence.transformation_size() + |
| fuzzer_options->replay_range)); |
| } |
| |
| auto replay_result = |
| spvtools::fuzz::Replayer( |
| target_env, spvtools::utils::CLIMessageConsumer, binary_in, |
| initial_facts, transformation_sequence, num_transformations_to_apply, |
| fuzzer_options->replay_validation_enabled, validator_options) |
| .Run(); |
| replay_result.transformed_module->module()->ToBinary(binary_out, false); |
| *transformations_applied = std::move(replay_result.applied_transformations); |
| return replay_result.status == |
| spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete; |
| } |
| |
| bool Shrink(const spv_target_env& target_env, |
| spv_const_fuzzer_options fuzzer_options, |
| spv_validator_options validator_options, |
| const std::vector<uint32_t>& binary_in, |
| const spvtools::fuzz::protobufs::FactSequence& initial_facts, |
| const std::string& shrink_transformations_file, |
| const std::string& shrink_temp_file_prefix, |
| const std::vector<std::string>& interestingness_command, |
| 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; |
| } |
| assert(!interestingness_command.empty() && |
| "An error should have been raised because the interestingness_command " |
| "is empty."); |
| std::stringstream joined; |
| joined << interestingness_command[0]; |
| for (size_t i = 1, size = interestingness_command.size(); i < size; ++i) { |
| joined << " " << interestingness_command[i]; |
| } |
| std::string interestingness_command_joined = joined.str(); |
| |
| spvtools::fuzz::Shrinker::InterestingnessFunction interestingness_function = |
| [interestingness_command_joined, shrink_temp_file_prefix]( |
| std::vector<uint32_t> binary, uint32_t reductions_applied) -> bool { |
| std::stringstream ss; |
| ss << shrink_temp_file_prefix << std::setw(4) << std::setfill('0') |
| << reductions_applied << ".spv"; |
| const auto spv_file = ss.str(); |
| const std::string command = interestingness_command_joined + " " + 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 = |
| spvtools::fuzz::Shrinker( |
| target_env, spvtools::utils::CLIMessageConsumer, binary_in, |
| initial_facts, transformation_sequence, interestingness_function, |
| fuzzer_options->shrinker_step_limit, |
| fuzzer_options->replay_validation_enabled, validator_options) |
| .Run(); |
| |
| *binary_out = std::move(shrink_result.transformed_binary); |
| *transformations_applied = std::move(shrink_result.applied_transformations); |
| 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, |
| spv_const_fuzzer_options fuzzer_options, |
| spv_validator_options validator_options, |
| const std::vector<uint32_t>& binary_in, |
| const spvtools::fuzz::protobufs::FactSequence& initial_facts, |
| const std::string& donors, |
| spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy, |
| FuzzingTarget fuzzing_target, std::vector<uint32_t>* binary_out, |
| spvtools::fuzz::protobufs::TransformationSequence* |
| transformations_applied) { |
| auto message_consumer = spvtools::utils::CLIMessageConsumer; |
| |
| std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> donor_suppliers; |
| |
| std::ifstream donors_file(donors); |
| if (!donors_file) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error opening donors file"); |
| return false; |
| } |
| std::string donor_filename; |
| while (std::getline(donors_file, donor_filename)) { |
| donor_suppliers.emplace_back( |
| [donor_filename, message_consumer, |
| target_env]() -> std::unique_ptr<spvtools::opt::IRContext> { |
| std::vector<uint32_t> donor_binary; |
| if (!ReadBinaryFile<uint32_t>(donor_filename.c_str(), |
| &donor_binary)) { |
| return nullptr; |
| } |
| return spvtools::BuildModule(target_env, message_consumer, |
| donor_binary.data(), |
| donor_binary.size()); |
| }); |
| } |
| |
| std::unique_ptr<spvtools::opt::IRContext> ir_context; |
| if (!spvtools::fuzz::fuzzerutil::BuildIRContext(target_env, message_consumer, |
| binary_in, validator_options, |
| &ir_context)) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, "Initial binary is invalid"); |
| return false; |
| } |
| |
| assert((fuzzing_target == FuzzingTarget::kWgsl || |
| fuzzing_target == FuzzingTarget::kSpirv) && |
| "Not all fuzzing targets are handled"); |
| auto fuzzer_context = spvtools::MakeUnique<spvtools::fuzz::FuzzerContext>( |
| spvtools::MakeUnique<spvtools::fuzz::PseudoRandomGenerator>( |
| fuzzer_options->has_random_seed |
| ? fuzzer_options->random_seed |
| : static_cast<uint32_t>(std::random_device()())), |
| spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()), |
| fuzzing_target == FuzzingTarget::kWgsl); |
| |
| auto transformation_context = |
| spvtools::MakeUnique<spvtools::fuzz::TransformationContext>( |
| spvtools::MakeUnique<spvtools::fuzz::FactManager>(ir_context.get()), |
| validator_options); |
| transformation_context->GetFactManager()->AddInitialFacts(message_consumer, |
| initial_facts); |
| |
| spvtools::fuzz::Fuzzer fuzzer( |
| std::move(ir_context), std::move(transformation_context), |
| std::move(fuzzer_context), message_consumer, donor_suppliers, |
| fuzzer_options->all_passes_enabled, repeated_pass_strategy, |
| fuzzer_options->fuzzer_pass_validation_enabled, validator_options, false); |
| auto fuzz_result = fuzzer.Run(0); |
| if (fuzz_result.status == |
| spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule) { |
| spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error running fuzzer"); |
| return false; |
| } |
| |
| fuzzer.GetIRContext()->module()->ToBinary(binary_out, true); |
| *transformations_applied = fuzzer.GetTransformationSequence(); |
| return true; |
| } |
| |
| } // namespace |
| |
| // Dumps |binary| to file |filename|. Useful for interactive debugging. |
| void DumpShader(const std::vector<uint32_t>& binary, const char* filename) { |
| auto write_file_succeeded = |
| WriteFile(filename, "wb", &binary[0], binary.size()); |
| if (!write_file_succeeded) { |
| std::cerr << "Failed to dump shader" << std::endl; |
| } |
| } |
| |
| // Dumps the SPIRV-V module in |context| to file |filename|. Useful for |
| // interactive debugging. |
| void DumpShader(spvtools::opt::IRContext* context, const char* filename) { |
| std::vector<uint32_t> binary; |
| context->module()->ToBinary(&binary, false); |
| DumpShader(binary, filename); |
| } |
| |
| // Dumps |transformations| to file |filename| in binary format. Useful for |
| // interactive debugging. |
| void DumpTransformationsBinary( |
| const spvtools::fuzz::protobufs::TransformationSequence& transformations, |
| const char* filename) { |
| std::ofstream transformations_file; |
| transformations_file.open(filename, std::ios::out | std::ios::binary); |
| transformations.SerializeToOstream(&transformations_file); |
| transformations_file.close(); |
| } |
| |
| // Dumps |transformations| to file |filename| in JSON format. Useful for |
| // interactive debugging. |
| void DumpTransformationsJson( |
| const spvtools::fuzz::protobufs::TransformationSequence& transformations, |
| const char* filename) { |
| 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, &json_string, json_options); |
| if (json_generation_status.ok()) { |
| std::ofstream transformations_json_file(filename); |
| transformations_json_file << json_string; |
| transformations_json_file.close(); |
| } |
| } |
| |
| 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 donors_file; |
| std::string replay_transformations_file; |
| std::vector<std::string> interestingness_test; |
| std::string shrink_transformations_file; |
| std::string shrink_temp_file_prefix = "temp_"; |
| spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy; |
| auto fuzzing_target = FuzzingTarget::kSpirv; |
| |
| spvtools::FuzzerOptions fuzzer_options; |
| spvtools::ValidatorOptions validator_options; |
| |
| FuzzStatus status = |
| ParseFlags(argc, argv, &in_binary_file, &out_binary_file, &donors_file, |
| &replay_transformations_file, &interestingness_test, |
| &shrink_transformations_file, &shrink_temp_file_prefix, |
| &repeated_pass_strategy, &fuzzing_target, &fuzzer_options, |
| &validator_options); |
| |
| if (status.action == FuzzActions::STOP) { |
| return status.code; |
| } |
| |
| std::vector<uint32_t> binary_in; |
| if (!ReadBinaryFile<uint32_t>(in_binary_file.c_str(), &binary_in)) { |
| return 1; |
| } |
| |
| spvtools::fuzz::protobufs::FactSequence initial_facts; |
| |
| // If not found, dot_pos will be std::string::npos, which can be used in |
| // substr to mean "the end of the string"; there is no need to check the |
| // result. |
| size_t dot_pos = in_binary_file.rfind('.'); |
| std::string in_facts_file = in_binary_file.substr(0, dot_pos) + ".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::JsonStringToMessage(facts_json_string, |
| &initial_facts) |
| .ok()) { |
| 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::FORCE_RENDER_RED: |
| if (!spvtools::fuzz::ForceRenderRed( |
| target_env, validator_options, binary_in, initial_facts, |
| spvtools::utils::CLIMessageConsumer, &binary_out)) { |
| return 1; |
| } |
| break; |
| case FuzzActions::FUZZ: |
| if (!Fuzz(target_env, fuzzer_options, validator_options, binary_in, |
| initial_facts, donors_file, repeated_pass_strategy, |
| fuzzing_target, &binary_out, &transformations_applied)) { |
| return 1; |
| } |
| break; |
| case FuzzActions::REPLAY: |
| if (!Replay(target_env, fuzzer_options, validator_options, 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, validator_options, binary_in, |
| initial_facts, shrink_transformations_file, |
| shrink_temp_file_prefix, interestingness_test, &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; |
| } |
| |
| if (status.action != FuzzActions::FORCE_RENDER_RED) { |
| // If not found, dot_pos will be std::string::npos, which can be used in |
| // substr to mean "the end of the string"; there is no need to check the |
| // result. |
| dot_pos = out_binary_file.rfind('.'); |
| std::string output_file_prefix = out_binary_file.substr(0, dot_pos); |
| 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.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; |
| } |