diff --git a/.gitignore b/.gitignore index 31c9126955..319c459c0a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ third_party/binutils third_party/googletest third_party/gpuweb-cts third_party/llvm-build +third_party/protobuf third_party/spirv-headers third_party/spirv-tools tools/clang diff --git a/AUTHORS b/AUTHORS index b4223536c5..a66d09ee44 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,3 +5,4 @@ # of contributors, see the revision history in source control. Google LLC +Vasyl Teliman diff --git a/CMakeLists.txt b/CMakeLists.txt index 8eb4e452bb..61a7fffad6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ option(TINT_BUILD_MSL_WRITER "Build the MSL output writer" ON) option(TINT_BUILD_SPV_WRITER "Build the SPIR-V output writer" ON) option(TINT_BUILD_WGSL_WRITER "Build the WGSL output writer" ON) option(TINT_BUILD_FUZZERS "Build fuzzers" OFF) +option(TINT_BUILD_SPIRV_TOOLS_FUZZER "Build SPIRV-Tools fuzzer" OFF) option(TINT_BUILD_TESTS "Build tests" ${TINT_BUILD_TESTS_DEFAULT}) option(TINT_BUILD_AS_OTHER_OS "Override OS detection to force building of *_other.cc files" OFF) @@ -68,6 +69,7 @@ message(STATUS "Tint build MSL writer: ${TINT_BUILD_MSL_WRITER}") message(STATUS "Tint build SPIR-V writer: ${TINT_BUILD_SPV_WRITER}") message(STATUS "Tint build WGSL writer: ${TINT_BUILD_WGSL_WRITER}") message(STATUS "Tint build fuzzers: ${TINT_BUILD_FUZZERS}") +message(STATUS "Tint build SPIRV-Tools fuzzer: ${TINT_BUILD_SPIRV_TOOLS_FUZZER}") message(STATUS "Tint build tests: ${TINT_BUILD_TESTS}") message(STATUS "Tint build with ASAN: ${TINT_ENABLE_ASAN}") message(STATUS "Tint build with MSAN: ${TINT_ENABLE_MSAN}") @@ -77,12 +79,30 @@ message(STATUS "Tint build checking [chromium-style]: ${TINT_CHECK_CHROMIUM_STYL message(STATUS "Using python3") find_package(PythonInterp 3 REQUIRED) +if (${TINT_BUILD_SPIRV_TOOLS_FUZZER}) + message(STATUS "TINT_BUILD_SPIRV_TOOLS_FUZZER is ON - setting + TINT_BUILD_FUZZERS, + TINT_BUILD_SPV_READER, + TINT_BUILD_WGSL_READER, + TINT_BUILD_WGSL_WRITER, + TINT_BUILD_HLSL_WRITER, + TINT_BUILD_MSL_WRITER, + TINT_BUILD_SPV_WRITER to ON") + set(TINT_BUILD_FUZZERS ON) + set(TINT_BUILD_SPV_READER ON) + set(TINT_BUILD_WGSL_READER ON) + set(TINT_BUILD_WGSL_WRITER ON) + set(TINT_BUILD_HLSL_WRITER ON) + set(TINT_BUILD_MSL_WRITER ON) + set(TINT_BUILD_SPV_WRITER ON) +endif() + # CMake < 3.15 sets /W3 in CMAKE_CXX_FLAGS. Remove it if it's there. # See https://gitlab.kitware.com/cmake/cmake/-/issues/18317 if (MSVC) - if (CMAKE_CXX_FLAGS MATCHES "/W3") - string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - endif() + if (CMAKE_CXX_FLAGS MATCHES "/W3") + string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + endif() endif() if (${TINT_CHECK_CHROMIUM_STYLE}) diff --git a/DEPS b/DEPS index 110c2b4710..d3b3dcbb30 100644 --- a/DEPS +++ b/DEPS @@ -11,6 +11,7 @@ vars = { 'clang_revision': 'eb5ab41f3801e2085208204fd71a490573d72dfd', 'googletest_revision': '5c8ca58edfb304b2dd5e6061f83387470826dd87', 'gpuweb_cts_revision': '177a4faf0a7ce6f8c64b42a715c634e363912a74', + 'protobuf_revision': 'fde7cf7358ec7cd69e8db9be4f1fa6a5c431386a', 'spirv_headers_revision': 'f5417a4b6633c3217c9a1bc2f0c70b1454975ba7', 'spirv_tools_revision': 'ecdd9a3e6bd384bf51d096b507291faa10f14685', 'testing_revision': '2691851e49de541c3fe42fa8692ddcdee938162f', @@ -42,6 +43,9 @@ deps = { 'third_party/googletest': Var('chromium_git') + Var('github') + '/google/googletest.git@' + Var('googletest_revision'), + + 'third_party/protobuf': Var('chromium_git') + Var('github') + + '/protocolbuffers/protobuf.git@' + Var('protobuf_revision'), } hooks = [ diff --git a/Doxyfile b/Doxyfile index 028d0ef17b..2e1c7aa719 100644 --- a/Doxyfile +++ b/Doxyfile @@ -786,7 +786,8 @@ WARN_LOGFILE = # Note: If this tag is empty the current directory is searched. INPUT = CODE_OF_CONDUCT.md \ - src + src \ + fuzzers/tint_spirv_tools_fuzzer # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/fuzzers/CMakeLists.txt b/fuzzers/CMakeLists.txt index 34e927e2de..8588b4540c 100644 --- a/fuzzers/CMakeLists.txt +++ b/fuzzers/CMakeLists.txt @@ -76,3 +76,7 @@ endif() if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER}) add_tint_fuzzer(tint_ast_clone_fuzzer) endif() + +if (${TINT_BUILD_SPIRV_TOOLS_FUZZER}) + add_subdirectory(tint_spirv_tools_fuzzer) +endif() diff --git a/fuzzers/tint_common_fuzzer.cc b/fuzzers/tint_common_fuzzer.cc index 6c92306b33..7d5879c86c 100644 --- a/fuzzers/tint_common_fuzzer.cc +++ b/fuzzers/tint_common_fuzzer.cc @@ -21,6 +21,7 @@ #include #include "src/ast/module.h" +#include "src/diagnostic/formatter.h" #include "src/program.h" namespace tint { @@ -191,6 +192,7 @@ int CommonFuzzer::Run(const uint8_t* data, size_t size) { } if (!program.IsValid()) { + errors_ = diag::Formatter().format(program.Diagnostics()); return 0; } @@ -199,58 +201,68 @@ int CommonFuzzer::Run(const uint8_t* data, size_t size) { auto entry_points = inspector.GetEntryPoints(); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } for (auto& ep : entry_points) { auto remapped_name = inspector.GetRemappedNameForEntryPoint(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto constant_ids = inspector.GetConstantIDs(); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto uniform_bindings = inspector.GetUniformBufferResourceBindings(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto storage_bindings = inspector.GetStorageBufferResourceBindings(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto readonly_bindings = inspector.GetReadOnlyStorageBufferResourceBindings(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto sampler_bindings = inspector.GetSamplerResourceBindings(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto comparison_sampler_bindings = inspector.GetComparisonSamplerResourceBindings(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto sampled_texture_bindings = inspector.GetSampledTextureResourceBindings(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } auto multisampled_texture_bindings = inspector.GetMultisampledTextureResourceBindings(ep.name); if (inspector.has_error()) { + errors_ = inspector.error(); return 0; } } @@ -272,39 +284,44 @@ int CommonFuzzer::Run(const uint8_t* data, size_t size) { program = std::move(out.program); } - std::unique_ptr writer; - switch (output_) { case OutputFormat::kWGSL: #if TINT_BUILD_WGSL_WRITER - writer = std::make_unique(&program); + writer_ = std::make_unique(&program); #endif // TINT_BUILD_WGSL_WRITER break; case OutputFormat::kSpv: #if TINT_BUILD_SPV_WRITER - writer = std::make_unique(&program); + writer_ = std::make_unique(&program); #endif // TINT_BUILD_SPV_WRITER break; case OutputFormat::kHLSL: #if TINT_BUILD_HLSL_WRITER - writer = std::make_unique(&program); + writer_ = std::make_unique(&program); #endif // TINT_BUILD_HLSL_WRITER break; case OutputFormat::kMSL: #if TINT_BUILD_MSL_WRITER - writer = std::make_unique(&program); + writer_ = std::make_unique(&program); #endif // TINT_BUILD_MSL_WRITER break; case OutputFormat::kNone: break; } - if (writer) { - writer->Generate(); + if (writer_) { + if (!writer_->Generate()) { + errors_ = writer_->error(); + return 0; + } } return 0; } +const writer::Writer* CommonFuzzer::GetWriter() const { + return writer_.get(); +} + } // namespace fuzzers } // namespace tint diff --git a/fuzzers/tint_common_fuzzer.h b/fuzzers/tint_common_fuzzer.h index 372df3561a..9201850a41 100644 --- a/fuzzers/tint_common_fuzzer.h +++ b/fuzzers/tint_common_fuzzer.h @@ -16,6 +16,7 @@ #define FUZZERS_TINT_COMMON_FUZZER_H_ #include +#include #include #include #include @@ -108,12 +109,20 @@ class CommonFuzzer { int Run(const uint8_t* data, size_t size); + const writer::Writer* GetWriter() const; + + const std::string& GetErrors() const { return errors_; } + + bool HasErrors() const { return !errors_.empty(); } + private: InputFormat input_; OutputFormat output_; + std::unique_ptr writer_; transform::Manager* transform_manager_; transform::DataMap transform_inputs_; bool inspector_enabled_; + std::string errors_; }; } // namespace fuzzers diff --git a/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt b/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt new file mode 100644 index 0000000000..92790c11a0 --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt @@ -0,0 +1,81 @@ +# Copyright 2021 The Tint Authors. +# +# 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. + +set(FUZZER_SOURCES + cli.cc + fuzzer.cc + mutator.cc + mutator_cache.cc + spirv_fuzz_mutator.cc + spirv_opt_mutator.cc + spirv_reduce_mutator.cc + util.cc) + +set(FUZZER_SOURCES ${FUZZER_SOURCES} + cli.h + mutator.h + mutator_cache.h + spirv_fuzz_mutator.h + spirv_opt_mutator.h + spirv_reduce_mutator.h + util.h) + +set(FUZZER_SOURCES ${FUZZER_SOURCES} + ../tint_common_fuzzer.h + ../tint_common_fuzzer.cc) + +function(configure_spirv_tools_fuzzer_target NAME SOURCES) + add_executable(${NAME} ${SOURCES}) + target_link_libraries(${NAME} SPIRV-Tools SPIRV-Tools-opt SPIRV-Tools-fuzz SPIRV-Tools-reduce) + tint_default_compile_options(${NAME}) + target_compile_options(${NAME} PRIVATE + -Wno-missing-prototypes + -Wno-zero-as-null-pointer-constant + -Wno-reserved-id-macro + -Wno-sign-conversion + -Wno-extra-semi-stmt + -Wno-inconsistent-missing-destructor-override + -Wno-newline-eof + -Wno-old-style-cast + -Wno-weak-vtables + -Wno-undef) + target_include_directories(${NAME} PRIVATE + ${spirv-tools_SOURCE_DIR} + ${spirv-tools_BINARY_DIR}) +endfunction() + +configure_spirv_tools_fuzzer_target(tint_spirv_tools_fuzzer "${FUZZER_SOURCES}") +target_compile_definitions(tint_spirv_tools_fuzzer PUBLIC CUSTOM_MUTATOR) +target_compile_definitions(tint_spirv_tools_fuzzer PRIVATE TARGET_FUZZER) +target_link_libraries(tint_spirv_tools_fuzzer libtint-fuzz) + +set(DEBUGGER_SOURCES + cli.cc + mutator.cc + mutator_debugger.cc + spirv_fuzz_mutator.cc + spirv_opt_mutator.cc + spirv_reduce_mutator.cc + util.cc) + +set(DEBUGGER_SOURCES ${DEBUGGER_SOURCES} + cli.h + mutator.h + spirv_fuzz_mutator.h + spirv_opt_mutator.h + spirv_reduce_mutator.h + util.h) + +configure_spirv_tools_fuzzer_target(tint_spirv_tools_mutator_debugger "${DEBUGGER_SOURCES}") +target_compile_definitions(tint_spirv_tools_mutator_debugger PRIVATE TARGET_DEBUGGER) diff --git a/fuzzers/tint_spirv_tools_fuzzer/cli.cc b/fuzzers/tint_spirv_tools_fuzzer/cli.cc new file mode 100644 index 0000000000..cfb6633280 --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/cli.cc @@ -0,0 +1,467 @@ +// Copyright 2021 The Tint Authors. +// +// 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 "fuzzers/tint_spirv_tools_fuzzer/cli.h" + +#include +#include +#include +#include +#include +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/util.h" +#include "source/opt/build_module.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { +namespace { + +const char* const kMutatorParameters = R"( +Mutators' parameters: + + --donors= + A path to the text file with a list of paths to the + SPIR-V donor files. Check out the doc for the spirv-fuzz + to learn more about donor binaries. Donors are not used + by default. + + --enable_all_fuzzer_passes= + Whether to use all fuzzer passes or a randomly selected subset + of them. This must be one of `true` or `false` (without `). + By default it's `false`. + + --enable_all_reduce_passes= + Whether to use all reduction passes or a randomly selected subset + of them. This must be one of `true` or `false` (without `). + By default it's `false`. + + --opt_batch_size= + The maximum number of spirv-opt optimizations that + will be applied in a single mutation session (i.e. + a call to LLVMFuzzerCustomMutator). This must fit in + uint32_t. By default it's 6. + + --reduction_batch_size= + The maximum number of spirv-reduce reductions that + will be applied in a single mutation session (i.e. + a call to LLVMFuzzerCustomMutator). This must fit in + uint32_t. By default it's 3. + + --repeated_pass_strategy= + The strategy that will be used to recommend the next fuzzer + pass. This must be one of `simple`, `looped` or `random` + (without `). By default it's `simple`. Check out the doc for + spirv-fuzz to learn more. + + --transformation_batch_size= + The maximum number of spirv-fuzz transformations + that will be applied during a single mutation + session (i.e. a call to LLVMFuzzerCustomMutator). + This must fit in uint32_t. By default it's 3. + + --validate_after_each_fuzzer_pass= + Whether to validate SPIR-V binary after each fuzzer pass. + This must be one of `true` or `false` (without `). + By default it's `true`. Switch this to `false` if you experience + bad performance. + + --validate_after_each_opt_pass= + Whether to validate SPIR-V binary after each optimization pass. + This must be one of `true` or `false` (without `). + By default it's `true`. Switch this to `false` if you experience + bad performance. + + --validate_after_each_reduce_pass= + Whether to validate SPIR-V binary after each reduction pass. + This must be one of `true` or `false` (without `). + By default it's `true`. Switch this to `false` if you experience + bad performance. +)"; + +const char* const kFuzzerHelpMessage = R"( +This fuzzer uses SPIR-V binaries to fuzz the Tint compiler. It uses SPIRV-Tools +to mutate those binaries. The fuzzer works on a corpus of SPIR-V shaders. +For each shader from the corpus it uses one of `spirv-fuzz`, `spirv-reduce` or +`spirv-opt` to mutate it and then runs the shader through the Tint compiler in +two steps: +- Converts the mutated shader to WGSL. +- Converts WGSL to some target language specified in the CLI arguments. + +Below is a list of all supported parameters for this fuzzer. You may want to +run it with -help=1 to check out libfuzzer parameters. + +Fuzzer parameters: + + --error_dir + The directory that will be used to output invalid SPIR-V + binaries to. This is especially useful during debugging + mutators. The directory must have the following subdirectories: + - spv/ - will be used to output errors, produced during + the conversion from the SPIR-V to WGSL. + - wgsl/ - will be used to output errors, produced during + the conversion from the WGSL to `--fuzzing_target`. + - mutator/ - will be used to output errors, produced by + the mutators. + By default invalid files are not printed out. + + --fuzzing_target + The type of backend to target during fuzzing. This must + be one or a combination of `wgsl`, `spv`, `msl` or `hlsl` + (without `) separated by commas. By default it's + `wgsl,spv,msl,hlsl`. + + --help + Show this message. Note that there is also a -help=1 + parameter that will display libfuzzer's help message. + + --mutator_cache_size= + The maximum size of the cache that stores + mutation sessions. This must fit in uint32_t. + By default it's 20. + + --mutator_type= + Determines types of the mutators to run. This must be one or + a combination of `fuzz`, `opt`, `reduce` (without `) separated by + comma. If a combination is specified, each element in the + combination will have an equal chance of mutating a SPIR-V + binary during a mutation session (i.e. if no mutator exists + for that binary in the mutator cache). By default, the + parameter's value is `fuzz,opt,reduce`. +)"; + +const char* const kMutatorDebuggerHelpMessage = R"( +This tool is used to debug *mutators*. It uses CLI arguments similar to the +ones used by the fuzzer. To debug some mutator you just need to specify the +mutator type, the seed and the path to the SPIR-V binary that triggered the +error. This tool will run the mutator on the binary until the error is +produced or the mutator returns `kLimitReached`. + +Note that this is different from debugging the fuzzer by specifying input +files to test. The difference is that the latter will not execute any +mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this +tool is useful when one of the SPIRV-Tools mutators crashes or produces an +invalid binary in LLVMFuzzerCustomMutator. + +Debugger parameters: + + --help + Show this message. + + --mutator_type= + Determines the type of the mutator to debug. This must be + one of `fuzz`, `reduce` or `opt` (without `). This parameter + is REQUIRED. + + --original_binary= + The path to the SPIR-V binary that the faulty mutator was + initialized with. This will be dumped on errors by the fuzzer + if `--error_dir` is specified. This parameter is REQUIRED. + + --seed= + The seed for the random number generator that was used to + initialize the mutator. This value is usually printed to + the console when the mutator produces an invalid binary. + It is also dumped into the log file if `--error_dir` is + specified. This must fit in uint32_t. This parameter is + REQUIRED. +)"; + +void PrintHelpMessage(const char* help_message) { + std::cout << help_message << std::endl << kMutatorParameters << std::endl; +} + +[[noreturn]] void InvalidParameter(const char* help_message, + const char* param) { + std::cout << "Invalid value for " << param << std::endl; + PrintHelpMessage(help_message); + exit(1); +} + +bool ParseUint32(const char* param, uint32_t* out) { + auto value = strtoul(param, nullptr, 10); + if (value > std::numeric_limits::max()) { + return false; + } + *out = static_cast(value); + return true; +} + +std::vector ParseDonors( + const char* file_name) { + std::ifstream fin(file_name); + if (!fin) { + std::cout << "Can't open donors list file: " << file_name << std::endl; + exit(1); + } + + std::vector result; + for (std::string donor_file_name; fin >> donor_file_name;) { + if (!std::ifstream(donor_file_name)) { + std::cout << "Can't open donor file: " << donor_file_name << std::endl; + exit(1); + } + + result.emplace_back([donor_file_name] { + std::vector binary; + if (!util::ReadBinary(donor_file_name, &binary)) { + std::cout << "Failed to read donor from: " << donor_file_name + << std::endl; + exit(1); + } + return spvtools::BuildModule( + kDefaultTargetEnv, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer, + binary.data(), binary.size()); + }); + } + + return result; +} + +bool ParseRepeatedPassStrategy(const char* param, + spvtools::fuzz::RepeatedPassStrategy* out) { + if (!strcmp(param, "simple")) { + *out = spvtools::fuzz::RepeatedPassStrategy::kSimple; + } else if (!strcmp(param, "looped")) { + *out = spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations; + } else if (!strcmp(param, "random")) { + *out = spvtools::fuzz::RepeatedPassStrategy::kRandomWithRecommendations; + } else { + return false; + } + return true; +} + +bool ParseBool(const char* param, bool* out) { + if (!strcmp(param, "true")) { + *out = true; + } else if (!strcmp(param, "false")) { + *out = false; + } else { + return false; + } + return true; +} + +bool ParseMutatorType(const char* param, MutatorType* out) { + if (!strcmp(param, "fuzz")) { + *out = MutatorType::kFuzz; + } else if (!strcmp(param, "opt")) { + *out = MutatorType::kOpt; + } else if (!strcmp(param, "reduce")) { + *out = MutatorType::kReduce; + } else { + return false; + } + return true; +} + +bool ParseFuzzingTarget(const char* param, FuzzingTarget* out) { + if (!strcmp(param, "wgsl")) { + *out = FuzzingTarget::kWgsl; + } else if (!strcmp(param, "spv")) { + *out = FuzzingTarget::kSpv; + } else if (!strcmp(param, "msl")) { + *out = FuzzingTarget::kMsl; + } else if (!strcmp(param, "hlsl")) { + *out = FuzzingTarget::kHlsl; + } else { + return false; + } + return true; +} + +bool HasPrefix(const char* str, const char* prefix) { + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +void ParseMutatorCliParam(const char* param, + const char* help_message, + MutatorCliParams* out) { + if (HasPrefix(param, "--transformation_batch_size=")) { + if (!ParseUint32(param + sizeof("--transformation_batch_size=") - 1, + &out->transformation_batch_size)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--reduction_batch_size=")) { + if (!ParseUint32(param + sizeof("--reduction_batch_size=") - 1, + &out->reduction_batch_size)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--opt_batch_size=")) { + if (!ParseUint32(param + sizeof("--opt_batch_size=") - 1, + &out->opt_batch_size)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--donors=")) { + out->donors = ParseDonors(param + sizeof("--donors=") - 1); + } else if (HasPrefix(param, "--repeated_pass_strategy=")) { + if (!ParseRepeatedPassStrategy( + param + sizeof("--repeated_pass_strategy=") - 1, + &out->repeated_pass_strategy)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--enable_all_fuzzer_passes=")) { + if (!ParseBool(param + sizeof("--enable_all_fuzzer_passes=") - 1, + &out->enable_all_fuzzer_passes)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--enable_all_reduce_passes=")) { + if (!ParseBool(param + sizeof("--enable_all_reduce_passes=") - 1, + &out->enable_all_reduce_passes)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--validate_after_each_opt_pass=")) { + if (!ParseBool(param + sizeof("--validate_after_each_opt_pass=") - 1, + &out->validate_after_each_opt_pass)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--validate_after_each_fuzzer_pass=")) { + if (!ParseBool(param + sizeof("--validate_after_each_fuzzer_pass=") - 1, + &out->validate_after_each_fuzzer_pass)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--validate_after_each_reduce_pass=")) { + if (!ParseBool(param + sizeof("--validate_after_each_reduce_pass=") - 1, + &out->validate_after_each_reduce_pass)) { + InvalidParameter(help_message, param); + } + } +} + +} // namespace + +FuzzerCliParams ParseFuzzerCliParams(int argc, const char* const* argv) { + FuzzerCliParams cli_params; + const auto* help_message = kFuzzerHelpMessage; + auto help = false; + + for (int i = 0; i < argc; ++i) { + auto param = argv[i]; + ParseMutatorCliParam(param, help_message, &cli_params.mutator_params); + + if (HasPrefix(param, "--mutator_cache_size=")) { + if (!ParseUint32(param + sizeof("--mutator_cache_size=") - 1, + &cli_params.mutator_cache_size)) { + InvalidParameter(help_message, param); + } + } else if (HasPrefix(param, "--mutator_type=")) { + auto result = MutatorType::kNone; + + std::stringstream ss(param + sizeof("--mutator_type=") - 1); + for (std::string value; std::getline(ss, value, ',');) { + auto out = MutatorType::kNone; + if (!ParseMutatorType(value.c_str(), &out)) { + InvalidParameter(help_message, param); + } + result = result | out; + } + + if (result == MutatorType::kNone) { + InvalidParameter(help_message, param); + } + + cli_params.mutator_type = result; + } else if (HasPrefix(param, "--fuzzing_target=")) { + auto result = FuzzingTarget::kNone; + + std::stringstream ss(param + sizeof("--fuzzing_target=") - 1); + for (std::string value; std::getline(ss, value, ',');) { + auto tmp = FuzzingTarget::kNone; + if (!ParseFuzzingTarget(value.c_str(), &tmp)) { + InvalidParameter(help_message, param); + } + result = result | tmp; + } + + if (result == FuzzingTarget::kNone) { + InvalidParameter(help_message, param); + } + + cli_params.fuzzing_target = result; + } else if (HasPrefix(param, "--error_dir=")) { + cli_params.error_dir = param + sizeof("--error_dir=") - 1; + } else if (!strcmp(param, "--help")) { + help = true; + } + } + + if (help) { + PrintHelpMessage(help_message); + exit(0); + } + + return cli_params; +} + +MutatorDebuggerCliParams ParseMutatorDebuggerCliParams( + int argc, + const char* const* argv) { + MutatorDebuggerCliParams cli_params; + bool seed_param_present = false; + bool original_binary_param_present = false; + bool mutator_type_param_present = false; + const auto* help_message = kMutatorDebuggerHelpMessage; + auto help = false; + + for (int i = 0; i < argc; ++i) { + auto param = argv[i]; + ParseMutatorCliParam(param, help_message, &cli_params.mutator_params); + + if (HasPrefix(param, "--mutator_type=")) { + if (!ParseMutatorType(param + sizeof("--mutator_type=") - 1, + &cli_params.mutator_type)) { + InvalidParameter(help_message, param); + } + mutator_type_param_present = true; + } else if (HasPrefix(param, "--original_binary=")) { + if (!util::ReadBinary(param + sizeof("--original_binary=") - 1, + &cli_params.original_binary)) { + InvalidParameter(help_message, param); + } + original_binary_param_present = true; + } else if (HasPrefix(param, "--seed=")) { + if (!ParseUint32(param + sizeof("--seed=") - 1, &cli_params.seed)) { + InvalidParameter(help_message, param); + } + seed_param_present = true; + } else if (!strcmp(param, "--help")) { + help = true; + } + } + + if (help) { + PrintHelpMessage(help_message); + exit(0); + } + + std::pair required_params[] = { + {seed_param_present, "--seed"}, + {original_binary_param_present, "--original_binary"}, + {mutator_type_param_present, "--mutator_type"}}; + + for (auto required_param : required_params) { + if (!required_param.first) { + std::cout << required_param.second << " is missing" << std::endl; + exit(1); + } + } + + return cli_params; +} + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/cli.h b/fuzzers/tint_spirv_tools_fuzzer/cli.h new file mode 100644 index 0000000000..7913a6b6da --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/cli.h @@ -0,0 +1,124 @@ +// Copyright 2021 The Tint Authors. +// +// 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 FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_ +#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_ + +#include +#include + +#include "source/fuzz/fuzzer.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +/// Default SPIR-V environment that will be used during fuzzing. +const auto kDefaultTargetEnv = SPV_ENV_VULKAN_1_1; + +/// The type of the mutator to run. +enum class MutatorType { + kNone = 0, + kFuzz = 1 << 0, + kReduce = 1 << 1, + kOpt = 1 << 2, + kAll = kFuzz | kReduce | kOpt +}; + +inline MutatorType operator|(MutatorType a, MutatorType b) { + return static_cast(static_cast(a) | static_cast(b)); +} + +inline MutatorType operator&(MutatorType a, MutatorType b) { + return static_cast(static_cast(a) & static_cast(b)); +} + +/// Shading language to target during fuzzing. +enum class FuzzingTarget { + kNone = 0, + kHlsl = 1 << 0, + kMsl = 1 << 1, + kSpv = 1 << 2, + kWgsl = 1 << 3, + kAll = kHlsl | kMsl | kSpv | kWgsl +}; + +inline FuzzingTarget operator|(FuzzingTarget a, FuzzingTarget b) { + return static_cast(static_cast(a) | static_cast(b)); +} + +inline FuzzingTarget operator&(FuzzingTarget a, FuzzingTarget b) { + return static_cast(static_cast(a) & static_cast(b)); +} + +/// These parameters are accepted by various mutators and thus they are accepted +/// by both the fuzzer and the mutator debugger. +struct MutatorCliParams { + spv_target_env target_env = kDefaultTargetEnv; + uint32_t transformation_batch_size = 3; + uint32_t reduction_batch_size = 3; + uint32_t opt_batch_size = 6; + std::vector donors = {}; + spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy = + spvtools::fuzz::RepeatedPassStrategy::kSimple; + bool enable_all_fuzzer_passes = false; + bool enable_all_reduce_passes = false; + bool validate_after_each_opt_pass = true; + bool validate_after_each_fuzzer_pass = true; + bool validate_after_each_reduce_pass = true; +}; + +/// Parameters specific to the fuzzer. +struct FuzzerCliParams { + uint32_t mutator_cache_size = 20; + MutatorType mutator_type = MutatorType::kAll; + FuzzingTarget fuzzing_target = FuzzingTarget::kAll; + std::string error_dir; + MutatorCliParams mutator_params; +}; + +/// Parameters specific to the mutator debugger. +struct MutatorDebuggerCliParams { + MutatorType mutator_type = MutatorType::kNone; + uint32_t seed = 0; + std::vector original_binary; + MutatorCliParams mutator_params; +}; + +/// Parses CLI parameters for the fuzzer. This function exits with an error code +/// and a message is printed to the console if some parameter has invalid +/// format. You can pass `--help` to check out all available parameters. +/// +/// @param argc - the number of parameters (identical to the `argc` in `main` +/// function). +/// @param argv - array of C strings of parameters. +/// @return the parsed parameters. +FuzzerCliParams ParseFuzzerCliParams(int argc, const char* const* argv); + +/// Parses CLI parameters for the mutator debugger. This function exits with an +/// error code and a message is printed to the console if some parameter has +/// invalid format. You can pass `--help` to check out all available parameters. +/// +/// @param argc - the number of parameters (identical to the `argc` in `main` +/// function). +/// @param argv - array of C strings of parameters. +/// @return the parsed parameters. +MutatorDebuggerCliParams ParseMutatorDebuggerCliParams(int argc, + const char* const* argv); + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint + +#endif // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_ diff --git a/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc b/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc new file mode 100644 index 0000000000..30acdf471b --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc @@ -0,0 +1,216 @@ +// Copyright 2021 The Tint Authors. +// +// 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 +#include +#include +#include +#include + +#include "fuzzers/tint_common_fuzzer.h" +#include "fuzzers/tint_spirv_tools_fuzzer/cli.h" +#include "fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h" +#include "fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h" +#include "fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h" +#include "fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h" +#include "fuzzers/tint_spirv_tools_fuzzer/util.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { +namespace { + +struct Context { + const FuzzerCliParams params; + std::unique_ptr mutator_cache; +}; + +Context* context = nullptr; + +extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) { + auto params = ParseFuzzerCliParams(*argc, *argv); + auto mutator_cache = + params.mutator_cache_size + ? std::make_unique(params.mutator_cache_size) + : nullptr; + context = new Context{std::move(params), std::move(mutator_cache)}; + return 0; +} + +std::unique_ptr CreateMutator(const std::vector& binary, + unsigned seed) { + std::vector types; + types.reserve(3); + + // Determine which mutator we will be using for `binary` at random. + auto cli_mutator_type = context->params.mutator_type; + if ((MutatorType::kFuzz & cli_mutator_type) == MutatorType::kFuzz) { + types.push_back(MutatorType::kFuzz); + } + if ((MutatorType::kReduce & cli_mutator_type) == MutatorType::kReduce) { + types.push_back(MutatorType::kReduce); + } + if ((MutatorType::kOpt & cli_mutator_type) == MutatorType::kOpt) { + types.push_back(MutatorType::kOpt); + } + + assert(!types.empty() && "At least one mutator type must be specified"); + std::mt19937 rng(seed); + auto mutator_type = + types[std::uniform_int_distribution(0, types.size() - 1)(rng)]; + + const auto& mutator_params = context->params.mutator_params; + switch (mutator_type) { + case MutatorType::kFuzz: + return std::make_unique( + mutator_params.target_env, binary, seed, mutator_params.donors, + mutator_params.enable_all_fuzzer_passes, + mutator_params.repeated_pass_strategy, + mutator_params.validate_after_each_fuzzer_pass, + mutator_params.transformation_batch_size); + case MutatorType::kReduce: + return std::make_unique( + mutator_params.target_env, binary, seed, + mutator_params.reduction_batch_size, + mutator_params.enable_all_reduce_passes, + mutator_params.validate_after_each_reduce_pass); + case MutatorType::kOpt: + return std::make_unique( + mutator_params.target_env, seed, binary, + mutator_params.validate_after_each_opt_pass, + mutator_params.opt_batch_size); + default: + assert(false && "All mutator types must be handled above"); + return nullptr; + } +} + +extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, + size_t size, + size_t max_size, + unsigned seed) { + std::vector binary(size / sizeof(uint32_t)); + std::memcpy(binary.data(), data, size); + + MutatorCache dummy_cache(1); + auto* mutator_cache = context->mutator_cache.get(); + if (!mutator_cache) { + // Use a dummy cache if the user has decided not to use a real cache. + // The dummy cache will be destroyed when we return from this function but + // it will save us from writing all the `if (mutator_cache)` below. + mutator_cache = &dummy_cache; + } + + if (!mutator_cache->Get(binary)) { + // Assign a mutator to the binary if it doesn't have one yet. + mutator_cache->Put(binary, CreateMutator(binary, seed)); + } + + auto* mutator = mutator_cache->Get(binary); + assert(mutator && "Mutator must be present in the cache"); + + auto result = mutator->Mutate(); + + if (result.GetStatus() == Mutator::Status::kInvalid) { + // The binary is invalid - log the error and remove the mutator from the + // cache. + util::LogMutatorError(*mutator, context->params.error_dir); + mutator_cache->Remove(binary); + return 0; + } + + if (!result.IsChanged()) { + // The mutator didn't change the binary this time. This could be due to the + // fact that we've reached the number of mutations we can apply (e.g. the + // number of transformations in spirv-fuzz) or the mutator was just unlucky. + // Either way, there is no harm in destroying mutator and maybe trying again + // later (i.e. if libfuzzer decides to do so). + mutator_cache->Remove(binary); + return 0; + } + + // At this point the binary is valid and was changed by the mutator. + + auto mutated = mutator->GetBinary(); + auto mutated_bytes_size = mutated.size() * sizeof(uint32_t); + if (mutated_bytes_size > max_size) { + // The binary is too big. It's unlikely that we'll reduce its size by + // applying the mutator one more time. + mutator_cache->Remove(binary); + return 0; + } + + if (result.GetStatus() == Mutator::Status::kComplete) { + // Reassign the mutator to the mutated binary in the cache so that we can + // access later. + mutator_cache->Put(mutated, mutator_cache->Remove(binary)); + } else { + // If the binary is valid and was changed but is not `kComplete`, then the + // mutator has reached some limit on the number of mutations. + mutator_cache->Remove(binary); + } + + std::memcpy(data, mutated.data(), mutated_bytes_size); + return mutated_bytes_size; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + if (size == 0) { + return 0; + } + + CommonFuzzer spv_to_wgsl(InputFormat::kSpv, OutputFormat::kWGSL); + spv_to_wgsl.EnableInspector(); + spv_to_wgsl.Run(data, size); + if (spv_to_wgsl.HasErrors()) { + util::LogSpvError(spv_to_wgsl.GetErrors(), data, size, + context->params.error_dir); + return 0; + } + + const auto* writer = + static_cast(spv_to_wgsl.GetWriter()); + + assert(writer && writer->error().empty() && + "Errors should have already been handled"); + + auto wgsl = writer->result(); + + std::pair targets[] = { + {FuzzingTarget::kHlsl, OutputFormat::kHLSL}, + {FuzzingTarget::kMsl, OutputFormat::kMSL}, + {FuzzingTarget::kSpv, OutputFormat::kSpv}, + {FuzzingTarget::kWgsl, OutputFormat::kWGSL}}; + + for (auto target : targets) { + if ((target.first & context->params.fuzzing_target) != target.first) { + continue; + } + + CommonFuzzer fuzzer(InputFormat::kWGSL, target.second); + fuzzer.EnableInspector(); + fuzzer.Run(reinterpret_cast(wgsl.data()), wgsl.size()); + if (fuzzer.HasErrors()) { + util::LogWgslError(fuzzer.GetErrors(), data, size, wgsl, target.second, + context->params.error_dir); + } + } + + return 0; +} + +} // namespace +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/mutator.cc new file mode 100644 index 0000000000..76cae68b5e --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/mutator.cc @@ -0,0 +1,34 @@ +// Copyright 2021 The Tint Authors. +// +// 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 "fuzzers/tint_spirv_tools_fuzzer/mutator.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +// We need to define constructor here so that vtable is produced in this +// translation unit (see -Wweak-vtables clang flag). +Mutator::~Mutator() = default; + +Mutator::Result::Result(Status status, bool is_changed) + : status_(status), is_changed_(is_changed) { + assert((is_changed || status == Status::kStuck || + status == Status::kLimitReached) && + "Returning invalid result state"); +} + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator.h b/fuzzers/tint_spirv_tools_fuzzer/mutator.h new file mode 100644 index 0000000000..ee3eecf68e --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/mutator.h @@ -0,0 +1,108 @@ +// Copyright 2021 The Tint Authors. +// +// 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 FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_ +#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_ + +#include +#include +#include +#include + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +/// This is an interface that is used to define custom mutators based on the +/// SPIR-V tools. +class Mutator { + public: + /// The status of the mutation. + enum class Status { + /// Binary is valid, the limit is not reached - can mutate further. + kComplete, + + /// The binary is valid, the limit of mutations has been reached - + /// can't mutate further. + kLimitReached, + + /// The binary is valid, the limit is not reached but the mutator has spent + /// too much time without mutating anything - better to restart to make sure + /// we can make any progress. + kStuck, + + /// The binary is invalid - this is likely a bug in the mutator - must + /// abort. + kInvalid + }; + + /// Represents the result of the mutation. The following states are possible: + /// - if `IsChanged() == false`, then `GetStatus()` can be either + /// `kLimitReached` or `kStuck`. + /// - otherwise, any value of `Status` is possible. + class Result { + public: + /// Constructor. + /// @param status - the status of the mutation. + /// @param is_changed - whether the module was changed during mutation. + Result(Status status, bool is_changed); + + /// @return the status of the mutation. + Status GetStatus() const { return status_; } + + /// @return whether the module was changed during mutation. + bool IsChanged() const { return is_changed_; } + + private: + Status status_; + bool is_changed_; + }; + + /// Virtual destructor. + virtual ~Mutator(); + + /// Causes the mutator to apply a mutation. This method can be called + /// multiple times as long as the previous call didn't return + /// `Status::kInvalid`. + /// + /// @return the status of the mutation (e.g. success, error etc) and whether + /// the binary was changed during mutation. + virtual Result Mutate() = 0; + + /// Returns the mutated binary. The returned binary is guaranteed to be valid + /// iff the previous call to the `Mutate` method returned didn't return + /// `Status::kInvalid`. + /// + /// @return the mutated SPIR-V binary. It might be identical to the original + /// binary if `Result::IsChanged` returns `false`. + virtual std::vector GetBinary() const = 0; + + /// Returns errors, produced by the mutator. + /// + /// @param path - the directory to which the errors are printed to. No files + /// are created if the `path` is nullptr. + /// @param count - the number of the error. Files for this error will be + /// prefixed with `count`. + virtual void LogErrors(const std::string* path, uint32_t count) const = 0; + + /// @return errors encountered during the mutation. The returned string is + /// if there were no errors during mutation. + virtual std::string GetErrors() const = 0; +}; + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint + +#endif // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_ diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc new file mode 100644 index 0000000000..03849f7449 --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc @@ -0,0 +1,78 @@ +// Copyright 2021 The Tint Authors. +// +// 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 "fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +MutatorCache::MutatorCache(size_t max_size) + : map_(), entries_(), max_size_(max_size) { + assert(max_size && "`max_size` may not be 0"); +} + +MutatorCache::Value::pointer MutatorCache::Get(const Key& key) { + auto it = map_.find(key); + if (it == map_.end()) { + return nullptr; + } + UpdateUsage(it); + return entries_.front().second.get(); +} + +void MutatorCache::Put(const Key& key, Value value) { + assert(value && "Mutator cache can't have nullptr unique_ptr"); + auto it = map_.find(key); + if (it != map_.end()) { + it->second->second = std::move(value); + UpdateUsage(it); + } else { + if (map_.size() == max_size_) { + Remove(*entries_.back().first); + } + + entries_.emplace_front(nullptr, std::move(value)); + auto pair = map_.emplace(key, entries_.begin()); + assert(pair.second && "The key must be unique"); + entries_.front().first = &pair.first->first; + } +} + +MutatorCache::Value MutatorCache::Remove(const Key& key) { + auto it = map_.find(key); + if (it == map_.end()) { + return nullptr; + } + auto result = std::move(it->second->second); + entries_.erase(it->second); + map_.erase(it); + return result; +} + +size_t MutatorCache::KeyHash::operator()( + const std::vector& vec) const { + return std::hash()({vec.begin(), vec.end()}); +} + +void MutatorCache::UpdateUsage(Map::iterator it) { + auto entry = std::move(*it->second); + entries_.erase(it->second); + entries_.push_front(std::move(entry)); + it->second = entries_.begin(); +} + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h new file mode 100644 index 0000000000..90dd5ceeeb --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h @@ -0,0 +1,99 @@ +// Copyright 2021 The Tint Authors. +// +// 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 FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_ +#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_ + +#include +#include +#include +#include +#include +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +/// Implementation of a fixed size LRU cache. That is, when the number of +/// elements reaches a certain threshold, the element that wasn't used for the +/// longest period of time is removed from the cache when a new element is +/// inserted. All operations have amortized constant time complexity. +class MutatorCache { + public: + /// SPIR-V binary that is being mutated. + using Key = std::vector; + + /// Mutator that is used to mutate the `Key`. + using Value = std::unique_ptr; + + /// Constructor. + /// @param max_size - the maximum number of elements the cache can store. May + /// not be equal to 0. + explicit MutatorCache(size_t max_size); + + /// Retrieves a pointer to a value, associated with a given `key`. + /// + /// If the key is present in the cache, its usage is updated and the + /// (non-null) pointer to the value is returned. Otherwise, `nullptr` is + /// returned. + /// + /// @param key - may not exist in this cache. + /// @return non-`nullptr` pointer to a value if `key` exists in the cache. + /// @return `nullptr` if `key` doesn't exist in this cache. + Value::pointer Get(const Key& key); + + /// Inserts a `key`-`value` pair into the cache. + /// + /// If the `key` is already present, the `value` replaces the old value and + /// the usage of `key` is updated. If the `key` is not present, then: + /// - if the number of elements in the cache is equal to `max_size`, the + /// key-value pair, where the usage of the key wasn't updated for the + /// longest period of time, is removed from the cache. + /// - a new `key`-`value` pair is inserted into the cache. + /// + /// @param key - a key. + /// @param value - may not be a `nullptr`. + void Put(const Key& key, Value value); + + /// Removes `key` and an associated value from the cache. + /// + /// @param key - a key. + /// @return a non-`nullptr` pointer to the removed value, associated with + /// `key`. + /// @return `nullptr` if `key` is not present in the cache. + Value Remove(const Key& key); + + private: + struct KeyHash { + size_t operator()(const std::vector& vec) const; + }; + + using Entry = std::pair; + using Map = std::unordered_map::iterator, KeyHash>; + + void UpdateUsage(Map::iterator it); + + Map map_; + std::list entries_; + const size_t max_size_; +}; + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint + +#endif // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_ diff --git a/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc b/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc new file mode 100644 index 0000000000..26f0419948 --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc @@ -0,0 +1,84 @@ +// Copyright 2021 The Tint Authors. +// +// 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 +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/cli.h" +#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h" +#include "fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h" +#include "fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h" +#include "fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h" +#include "fuzzers/tint_spirv_tools_fuzzer/util.h" + +/// This tool is used to debug *mutators*. It uses CLI arguments similar to the +/// ones used by the fuzzer. To debug some mutator you just need to specify the +/// mutator type, the seed and the path to the SPIR-V binary that triggered the +/// error. This tool will run the mutator on the binary until the error is +/// produced or the mutator returns `kLimitReached`. +/// +/// Note that this is different from debugging the fuzzer by specifying input +/// files to test. The difference is that the latter will not execute any +/// mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this +/// tool is useful when one of the spirv-tools mutators crashes or produces an +/// invalid binary in LLVMFuzzerCustomMutator. +int main(int argc, const char** argv) { + auto params = + tint::fuzzers::spvtools_fuzzer::ParseMutatorDebuggerCliParams(argc, argv); + + std::unique_ptr mutator; + const auto& mutator_params = params.mutator_params; + switch (params.mutator_type) { + case tint::fuzzers::spvtools_fuzzer::MutatorType::kFuzz: + mutator = + std::make_unique( + mutator_params.target_env, params.original_binary, params.seed, + mutator_params.donors, mutator_params.enable_all_fuzzer_passes, + mutator_params.repeated_pass_strategy, + mutator_params.validate_after_each_fuzzer_pass, + mutator_params.transformation_batch_size); + break; + case tint::fuzzers::spvtools_fuzzer::MutatorType::kReduce: + mutator = + std::make_unique( + mutator_params.target_env, params.original_binary, params.seed, + mutator_params.reduction_batch_size, + mutator_params.enable_all_reduce_passes, + mutator_params.validate_after_each_reduce_pass); + break; + case tint::fuzzers::spvtools_fuzzer::MutatorType::kOpt: + mutator = + std::make_unique( + mutator_params.target_env, params.seed, params.original_binary, + mutator_params.validate_after_each_opt_pass, + mutator_params.opt_batch_size); + break; + default: + assert(false && "All mutator types must've been handled"); + return 1; + } + + while (true) { + auto result = mutator->Mutate(); + if (result.GetStatus() == + tint::fuzzers::spvtools_fuzzer::Mutator::Status::kInvalid) { + std::cerr << mutator->GetErrors() << std::endl; + return 0; + } + if (result.GetStatus() == + tint::fuzzers::spvtools_fuzzer::Mutator::Status::kLimitReached) { + break; + } + } +} diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc new file mode 100644 index 0000000000..dca10f1f22 --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc @@ -0,0 +1,127 @@ +// Copyright 2021 The Tint Authors. +// +// 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 "fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h" + +#include +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/util.h" +#include "source/opt/build_module.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +SpirvFuzzMutator::SpirvFuzzMutator( + spv_target_env target_env, + std::vector binary, + unsigned seed, + const std::vector& donors, + bool enable_all_passes, + spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy, + bool validate_after_each_pass, + uint32_t transformation_batch_size) + : transformation_batch_size_(transformation_batch_size), + errors_(std::make_unique()), + fuzzer_(nullptr), + validator_options_(), + original_binary_(std::move(binary)), + seed_(seed) { + auto ir_context = spvtools::BuildModule( + target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer, + original_binary_.data(), original_binary_.size()); + assert(ir_context && "|binary| is invalid"); + + auto transformation_context = + std::make_unique( + std::make_unique(ir_context.get()), + validator_options_); + + auto fuzzer_context = std::make_unique( + std::make_unique(seed), + spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()), false); + fuzzer_ = std::make_unique( + std::move(ir_context), std::move(transformation_context), + std::move(fuzzer_context), util::GetBufferMessageConsumer(errors_.get()), + donors, enable_all_passes, repeated_pass_strategy, + validate_after_each_pass, validator_options_); +} + +Mutator::Result SpirvFuzzMutator::Mutate() { + // The assertion will fail in |fuzzer_->Run| if the previous fuzzing led to + // invalid module. + auto result = fuzzer_->Run(transformation_batch_size_); + switch (result.status) { + case spvtools::fuzz::Fuzzer::Status::kComplete: + return {Mutator::Status::kComplete, result.is_changed}; + case spvtools::fuzz::Fuzzer::Status::kModuleTooBig: + case spvtools::fuzz::Fuzzer::Status::kTransformationLimitReached: + return {Mutator::Status::kLimitReached, result.is_changed}; + case spvtools::fuzz::Fuzzer::Status::kFuzzerStuck: + return {Mutator::Status::kStuck, result.is_changed}; + case spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule: + return {Mutator::Status::kInvalid, result.is_changed}; + } +} + +std::vector SpirvFuzzMutator::GetBinary() const { + std::vector result; + fuzzer_->GetIRContext()->module()->ToBinary(&result, true); + return result; +} + +std::string SpirvFuzzMutator::GetErrors() const { + return errors_->str(); +} + +void SpirvFuzzMutator::LogErrors(const std::string* path, + uint32_t count) const { + auto message = GetErrors(); + std::cout << count << " | SpirvFuzzMutator (seed: " << seed_ << ")" + << std::endl; + std::cout << message << std::endl; + + if (path) { + auto prefix = *path + std::to_string(count); + + // Write errors to file. + std::ofstream(prefix + ".fuzzer.log") << "seed: " << seed_ << std::endl + << message << std::endl; + + // Write the invalid SPIR-V binary. + util::WriteBinary(prefix + ".fuzzer.invalid.spv", GetBinary()); + + // Write the original SPIR-V binary. + util::WriteBinary(prefix + ".fuzzer.original.spv", original_binary_); + + // Write transformations. + google::protobuf::util::JsonOptions options; + options.add_whitespace = true; + std::string json; + google::protobuf::util::MessageToJsonString( + fuzzer_->GetTransformationSequence(), &json, options); + std::ofstream(prefix + ".fuzzer.transformations.json") << json << std::endl; + + std::ofstream binary_transformations( + prefix + ".fuzzer.transformations.binary", + std::ios::binary | std::ios::out); + fuzzer_->GetTransformationSequence().SerializeToOstream( + &binary_transformations); + } +} + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h new file mode 100644 index 0000000000..79aa07d249 --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h @@ -0,0 +1,95 @@ +// Copyright 2021 The Tint Authors. +// +// 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 FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_ +#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_ + +#include +#include +#include +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h" + +#include "source/fuzz/fuzzer.h" +#include "source/fuzz/protobufs/spirvfuzz_protobufs.h" +#include "source/fuzz/pseudo_random_generator.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +/// The mutator that uses spirv-fuzz to mutate SPIR-V. +/// +/// The initial `binary` must be valid according to `target_env`. All other +/// parameters (except for the `seed` which just initializes the RNG) are from +/// the `spvtools::fuzz::Fuzzer` class. +class SpirvFuzzMutator : public Mutator { + public: + /// Constructor. + /// @param target_env - the target environment for the `binary`. + /// @param binary - the SPIR-V binary. Must be valid. + /// @param seed - seed for the RNG. + /// @param donors - vector of donor suppliers. + /// @param enable_all_passes - whether to use all fuzzer passes. + /// @param repeated_pass_strategy - the strategy to use when selecting the + /// next fuzzer pass. + /// @param validate_after_each_pass - whether to validate the binary after + /// each fuzzer pass. + /// @param transformation_batch_size - the maximum number of transformations + /// that will be applied during a single call to `Mutate`. It it's equal + /// to 0 then we apply as much transformations as we can until the + /// threshold in the spvtools::fuzz::Fuzzer is reached (see the doc for + /// that class for more info). + SpirvFuzzMutator( + spv_target_env target_env, + std::vector binary, + uint32_t seed, + const std::vector& donors, + bool enable_all_passes, + spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy, + bool validate_after_each_pass, + uint32_t transformation_batch_size); + + Result Mutate() override; + std::vector GetBinary() const override; + void LogErrors(const std::string* path, uint32_t count) const override; + std::string GetErrors() const override; + + private: + // The number of transformations that will be applied during a single call to + // the `Mutate` method. Is this only a lower bound since transformations are + // applied in batches by fuzzer passes (see docs for the + // `spvtools::fuzz::Fuzzer` for more info). + const uint32_t transformation_batch_size_; + + // The errors produced by the `spvtools::fuzz::Fuzzer`. + std::unique_ptr errors_; + std::unique_ptr fuzzer_; + spvtools::ValidatorOptions validator_options_; + + // The following fields are useful for debugging. + + // The binary that the mutator is constructed with. + const std::vector original_binary_; + + // The seed that the mutator is constructed with. + const uint32_t seed_; +}; + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint + +#endif // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_ diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc new file mode 100644 index 0000000000..360f1d9d8a --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc @@ -0,0 +1,159 @@ +// Copyright 2021 The Tint Authors. +// +// 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 "fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h" + +#include +#include +#include +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/util.h" +#include "spirv-tools/optimizer.hpp" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +SpirvOptMutator::SpirvOptMutator(spv_target_env target_env, + uint32_t seed, + std::vector binary, + bool validate_after_each_opt, + uint32_t opt_batch_size) + : num_executions_(0), + is_valid_(true), + target_env_(target_env), + original_binary_(std::move(binary)), + seed_(seed), + opt_passes_({"--combine-access-chains", + "--loop-unroll", + "--merge-blocks", + "--cfg-cleanup", + "--eliminate-dead-functions", + "--merge-return", + "--wrap-opkill", + "--eliminate-dead-code-aggressive", + "--if-conversion", + "--eliminate-local-single-store", + "--eliminate-local-single-block", + "--eliminate-dead-branches", + "--scalar-replacement=0", + "--eliminate-dead-inserts", + "--eliminate-dead-members", + "--simplify-instructions", + "--private-to-local", + "--ssa-rewrite", + "--ccp", + "--reduce-load-size", + "--vector-dce", + "--scalar-replacement=100", + "--inline-entry-points-exhaustive", + "--redundancy-elimination", + "--convert-local-access-chains", + "--copy-propagate-arrays", + "--fix-storage-class"}), + optimized_binary_(), + validate_after_each_opt_(validate_after_each_opt), + opt_batch_size_(opt_batch_size), + rng_(seed) { + assert(spvtools::SpirvTools(target_env).Validate(original_binary_) && + "Initial binary is invalid"); + assert(!opt_passes_.empty() && "Must be at least one pass"); +} + +SpirvOptMutator::Result SpirvOptMutator::Mutate() { + assert(is_valid_ && "The optimizer is not longer valid"); + + const uint32_t kMaxNumExecutions = 100; + const uint32_t kMaxNumStuck = 10; + + if (num_executions_ == kMaxNumExecutions) { + // We've applied this mutator many times already. Indicate to the user that + // it might be better to try a different mutator. + return {Status::kLimitReached, false}; + } + + num_executions_++; + + // Get the input binary. If this is the first time we run this mutator, use + // the `original_binary_`. Otherwise, one of the following will be true: + // - the `optimized_binary_` is not empty. + // - the previous call to the `Mutate` method returned `kStuck`. + auto binary = num_executions_ == 1 ? original_binary_ : optimized_binary_; + optimized_binary_.clear(); + + assert(!binary.empty() && "Can't run the optimizer on an empty binary"); + + // Number of times spirv-opt wasn't able to produce any new result. + uint32_t num_stuck = 0; + do { + // Randomly select `opt_batch_size` optimization passes. If `opt_batch_size` + // is equal to 0, we will use the number of passes equal to the number of + // all available passes. + auto num_of_passes = opt_batch_size_ ? opt_batch_size_ : opt_passes_.size(); + std::vector passes; + + while (passes.size() < num_of_passes) { + auto idx = std::uniform_int_distribution( + 0, opt_passes_.size() - 1)(rng_); + passes.push_back(opt_passes_[idx]); + } + + // Run the `binary` into the `optimized_binary_`. + spvtools::Optimizer optimizer(target_env_); + optimizer.SetMessageConsumer(util::GetBufferMessageConsumer(&errors_)); + optimizer.SetValidateAfterAll(validate_after_each_opt_); + optimizer.RegisterPassesFromFlags(passes); + if (!optimizer.Run(binary.data(), binary.size(), &optimized_binary_)) { + is_valid_ = false; + return {Status::kInvalid, true}; + } + } while (optimized_binary_.empty() && ++num_stuck < kMaxNumStuck); + + return {optimized_binary_.empty() ? Status::kStuck : Status::kComplete, + !optimized_binary_.empty()}; +} + +std::vector SpirvOptMutator::GetBinary() const { + return optimized_binary_; +} + +std::string SpirvOptMutator::GetErrors() const { + return errors_.str(); +} + +void SpirvOptMutator::LogErrors(const std::string* path, uint32_t count) const { + auto message = GetErrors(); + std::cout << count << " | SpirvOptMutator (seed: " << seed_ << ")" + << std::endl; + std::cout << message << std::endl; + + if (path) { + auto prefix = *path + std::to_string(count); + + // Write errors to file. + std::ofstream(prefix + ".opt.log") << "seed: " << seed_ << std::endl + << message << std::endl; + + // Write the invalid SPIR-V binary. + util::WriteBinary(prefix + ".opt.invalid.spv", optimized_binary_); + + // Write the original SPIR-V binary. + util::WriteBinary(prefix + ".opt.original.spv", original_binary_); + } +} + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h new file mode 100644 index 0000000000..6d055e4b0e --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h @@ -0,0 +1,96 @@ +// Copyright 2021 The Tint Authors. +// +// 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 FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_ +#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_ + +#include +#include +#include +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h" +#include "spirv-tools/libspirv.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +/// Mutates the SPIR-V module using the spirv-opt tool. +/// +/// The initial `binary` must be valid according to `target_env`. On each call +/// to the `Mutate` method the mutator selects `opt_batch_size` random +/// optimization passes (with substitutions) and applies them to the binary. +class SpirvOptMutator : public Mutator { + public: + /// Constructor. + /// @param target_env - target environment for the `binary`. + /// @param seed - seed for the RNG. + /// @param binary - SPIR-V binary. Must be valid. + /// @param validate_after_each_opt - whether to validate the binary after each + /// optimization pass. + /// @param opt_batch_size - the maximum number of optimization passes that + /// will be applied in a single call to `Mutate`. If it's equal to 0 then + /// all available optimization passes are applied. + SpirvOptMutator(spv_target_env target_env, + uint32_t seed, + std::vector binary, + bool validate_after_each_opt, + uint32_t opt_batch_size); + + Result Mutate() override; + std::vector GetBinary() const override; + void LogErrors(const std::string* path, uint32_t count) const override; + std::string GetErrors() const override; + + private: + // Number of times this mutator was executed. + uint32_t num_executions_; + + // Whether the last execution left it in a valid state. + bool is_valid_; + + // Target environment for the SPIR-V binary. + const spv_target_env target_env_; + + // The original SPIR-V binary. Useful for debugging. + const std::vector original_binary_; + + // The seed for the RNG. Useful for debugging. + const uint32_t seed_; + + // All the optimization passes available. + const std::vector opt_passes_; + + // The result of the optimization. + std::vector optimized_binary_; + + // Whether we need to validate the binary after each optimization pass. + const bool validate_after_each_opt_; + + // The number of optimization passes to apply at once. + const uint32_t opt_batch_size_; + + // All the errors produced by the optimizer. + std::stringstream errors_; + + // The random number generator initialized with `seed_`. + std::mt19937 rng_; +}; + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint + +#endif // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_ diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc new file mode 100644 index 0000000000..dccbfe354f --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc @@ -0,0 +1,190 @@ +// Copyright 2021 The Tint Authors. +// +// 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 "fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h" + +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/util.h" +#include "source/fuzz/fuzzer_util.h" +#include "source/opt/build_module.h" +#include "source/reduce/conditional_branch_to_simple_conditional_branch_opportunity_finder.h" +#include "source/reduce/merge_blocks_reduction_opportunity_finder.h" +#include "source/reduce/operand_to_const_reduction_opportunity_finder.h" +#include "source/reduce/operand_to_dominating_id_reduction_opportunity_finder.h" +#include "source/reduce/operand_to_undef_reduction_opportunity_finder.h" +#include "source/reduce/remove_block_reduction_opportunity_finder.h" +#include "source/reduce/remove_function_reduction_opportunity_finder.h" +#include "source/reduce/remove_selection_reduction_opportunity_finder.h" +#include "source/reduce/remove_unused_instruction_reduction_opportunity_finder.h" +#include "source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h" +#include "source/reduce/simple_conditional_branch_to_branch_opportunity_finder.h" +#include "source/reduce/structured_loop_to_selection_reduction_opportunity_finder.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +SpirvReduceMutator::SpirvReduceMutator(spv_target_env target_env, + std::vector binary, + uint32_t seed, + uint32_t reductions_batch_size, + bool enable_all_reductions, + bool validate_after_each_reduction) + : ir_context_(nullptr), + finders_(), + rng_(seed), + errors_(), + is_valid_(true), + reductions_batch_size_(reductions_batch_size), + total_applied_reductions_(0), + enable_all_reductions_(enable_all_reductions), + validate_after_each_reduction_(validate_after_each_reduction), + original_binary_(std::move(binary)), + seed_(seed) { + ir_context_ = spvtools::BuildModule( + target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer, + original_binary_.data(), original_binary_.size()); + assert(ir_context_ && "|binary| is invalid"); + + do { + MaybeAddFinder< + spvtools::reduce:: + ConditionalBranchToSimpleConditionalBranchOpportunityFinder>(); + MaybeAddFinder(); + MaybeAddFinder< + spvtools::reduce::OperandToConstReductionOpportunityFinder>(); + MaybeAddFinder< + spvtools::reduce::OperandToDominatingIdReductionOpportunityFinder>(); + MaybeAddFinder< + spvtools::reduce::OperandToUndefReductionOpportunityFinder>(); + MaybeAddFinder(); + MaybeAddFinder< + spvtools::reduce::RemoveFunctionReductionOpportunityFinder>(); + MaybeAddFinder< + spvtools::reduce::RemoveSelectionReductionOpportunityFinder>(); + MaybeAddFinder< + spvtools::reduce::RemoveUnusedInstructionReductionOpportunityFinder>( + true); + MaybeAddFinder< + spvtools::reduce::RemoveUnusedStructMemberReductionOpportunityFinder>(); + MaybeAddFinder< + spvtools::reduce::SimpleConditionalBranchToBranchOpportunityFinder>(); + MaybeAddFinder(); + } while (finders_.empty()); +} + +Mutator::Result SpirvReduceMutator::Mutate() { + assert(is_valid_ && "Can't mutate invalid module"); + + // The upper limit on the number of applied reduction passes. + const uint32_t kMaxAppliedReductions = 500; + const auto old_applied_reductions = total_applied_reductions_; + + // The upper limit on the number of failed attempts to apply reductions (i.e. + // when no reduction was returned by the reduction finder). + const uint32_t kMaxConsecutiveFailures = 10; + uint32_t num_consecutive_failures = 0; + + // Iterate while we haven't exceeded the limit on the total number of applied + // reductions, the limit on the number of reductions applied at once and limit + // on the number of consecutive failed attempts. + while (total_applied_reductions_ < kMaxAppliedReductions && + (reductions_batch_size_ == 0 || + total_applied_reductions_ - old_applied_reductions < + reductions_batch_size_) && + num_consecutive_failures < kMaxConsecutiveFailures) { + // Select an opportunity finder and get some reduction opportunities from + // it. + auto finder = GetRandomElement(&finders_); + auto reduction_opportunities = + finder->GetAvailableOpportunities(ir_context_.get(), 0); + + if (reduction_opportunities.empty()) { + // There is nothing to reduce. We increase the counter to make sure we + // don't stuck in this situation. + num_consecutive_failures++; + } else { + // Apply a random reduction opportunity. The latter should be applicable. + auto opportunity = GetRandomElement(&reduction_opportunities); + assert(opportunity->PreconditionHolds() && "Preconditions should hold"); + total_applied_reductions_++; + num_consecutive_failures = 0; + if (!ApplyReduction(opportunity)) { + // The module became invalid as a result of the applied reduction. + is_valid_ = false; + return {Mutator::Status::kInvalid, + total_applied_reductions_ != old_applied_reductions}; + } + } + } + + auto is_changed = total_applied_reductions_ != old_applied_reductions; + if (total_applied_reductions_ == kMaxAppliedReductions) { + return {Mutator::Status::kLimitReached, is_changed}; + } + + if (num_consecutive_failures == kMaxConsecutiveFailures) { + return {Mutator::Status::kStuck, is_changed}; + } + + assert(is_changed && "This is the only way left to break the loop"); + return {Mutator::Status::kComplete, is_changed}; +} + +bool SpirvReduceMutator::ApplyReduction( + spvtools::reduce::ReductionOpportunity* reduction_opportunity) { + reduction_opportunity->TryToApply(); + return !validate_after_each_reduction_ || + spvtools::fuzz::fuzzerutil::IsValidAndWellFormed( + ir_context_.get(), spvtools::ValidatorOptions(), + util::GetBufferMessageConsumer(&errors_)); +} + +std::vector SpirvReduceMutator::GetBinary() const { + std::vector result; + ir_context_->module()->ToBinary(&result, true); + return result; +} + +std::string SpirvReduceMutator::GetErrors() const { + return errors_.str(); +} + +void SpirvReduceMutator::LogErrors(const std::string* path, + uint32_t count) const { + auto message = GetErrors(); + std::cout << count << " | SpirvReduceMutator (seed: " << seed_ << ")" + << std::endl; + std::cout << message << std::endl; + + if (path) { + auto prefix = *path + std::to_string(count); + + // Write errors to file. + std::ofstream(prefix + ".reducer.log") << "seed: " << seed_ << std::endl + << message << std::endl; + + // Write the invalid SPIR-V binary. + util::WriteBinary(prefix + ".reducer.invalid.spv", GetBinary()); + + // Write the original SPIR-V binary. + util::WriteBinary(prefix + ".reducer.original.spv", original_binary_); + } +} + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h new file mode 100644 index 0000000000..4d656d8f6a --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h @@ -0,0 +1,134 @@ +// Copyright 2021 The Tint Authors. +// +// 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 FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_ +#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_ + +#include +#include +#include +#include +#include +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h" + +#include "source/reduce/reduction_opportunity_finder.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { + +/// Mutates SPIR-V binary by running spirv-reduce tool. +/// +/// The initial `binary` must be valid according to `target_env`. Applies at +/// most `reductions_batch_size` reductions at a time. This parameter is ignored +/// if its value is 0. Uses a random subset of reduction opportunity finders by +/// default. This can be overridden with the `enable_all_reductions` parameter. +class SpirvReduceMutator : public Mutator { + public: + /// Constructor. + /// @param target_env - the target environment for the `binary`. + /// @param binary - SPIR-V binary. Must be valid. + /// @param seed - the seed for the RNG. + /// @param reductions_batch_size - the number of reduction passes that will be + /// applied during a single call to `Mutate`. If it's equal to 0 then we + /// apply the passes until we reach the threshold for the total number of + /// applied passes. + /// @param enable_all_reductions - whether to use all reduction passes or only + /// a randomly selected subset of them. + /// @param validate_after_each_reduction - whether to validate after each + /// applied reduction. + SpirvReduceMutator(spv_target_env target_env, + std::vector binary, + uint32_t seed, + uint32_t reductions_batch_size, + bool enable_all_reductions, + bool validate_after_each_reduction); + + Result Mutate() override; + std::vector GetBinary() const override; + void LogErrors(const std::string* path, uint32_t count) const override; + std::string GetErrors() const override; + + private: + template + void MaybeAddFinder(Args&&... args) { + if (enable_all_reductions_ || std::uniform_int_distribution<>(0, 1)(rng_)) { + finders_.push_back(std::make_unique(std::forward(args)...)); + } + } + + template + T* GetRandomElement(std::vector* arr) { + assert(!arr->empty() && "Can't get random element from an empty vector"); + auto index = + std::uniform_int_distribution(0, arr->size() - 1)(rng_); + return &(*arr)[index]; + } + + template + T* GetRandomElement(std::vector>* arr) { + assert(!arr->empty() && "Can't get random element from an empty vector"); + auto index = + std::uniform_int_distribution(0, arr->size() - 1)(rng_); + return (*arr)[index].get(); + } + + bool ApplyReduction( + spvtools::reduce::ReductionOpportunity* reduction_opportunity); + + // The SPIR-V binary that is being reduced. + std::unique_ptr ir_context_; + + // The selected subset of reduction opportunity finders. + std::vector> + finders_; + + // Random number generator initialized with `seed_`. + std::mt19937 rng_; + + // All the errors produced by the reducer. + std::stringstream errors_; + + // Whether the last call to the `Mutate` method produced the valid binary. + bool is_valid_; + + // The number of reductions to apply on a single call to `Mutate`. + const uint32_t reductions_batch_size_; + + // The total number of applied reductions. + uint32_t total_applied_reductions_; + + // Whether we want to use all the reduction opportunity finders and not just a + // subset of them. + const bool enable_all_reductions_; + + // Whether we want to validate all the binary after each reduction. + const bool validate_after_each_reduction_; + + // The original binary that was used to initialize this mutator. + // Useful for debugging. + const std::vector original_binary_; + + // The seed that was used to initialize the random number generator. + // Useful for debugging. + const uint32_t seed_; +}; + +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint + +#endif // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_ diff --git a/fuzzers/tint_spirv_tools_fuzzer/util.cc b/fuzzers/tint_spirv_tools_fuzzer/util.cc new file mode 100644 index 0000000000..bd574a2f2b --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/util.cc @@ -0,0 +1,160 @@ +// Copyright 2021 The Tint Authors. +// +// 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 +#include + +#include "fuzzers/tint_spirv_tools_fuzzer/util.h" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { +namespace util { +namespace { + +bool WriteBinary(const std::string& path, const uint8_t* data, size_t size) { + std::ofstream spv(path, std::ios::binary); + return spv && spv.write(reinterpret_cast(data), + static_cast(size)); +} + +void LogError(uint32_t index, + const std::string& type, + const std::string& message, + const std::string* path, + const uint8_t* data, + size_t size, + const std::string* wgsl) { + std::cout << index << " | " << type << ": " << message << std::endl; + + if (path) { + auto prefix = *path + std::to_string(index); + std::ofstream(prefix + ".log") << message << std::endl; + + WriteBinary(prefix + ".spv", data, size); + + if (wgsl) { + std::ofstream(prefix + ".wgsl") << *wgsl << std::endl; + } + } +} + +} // namespace + +spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer) { + return [buffer](spv_message_level_t level, const char*, + const spv_position_t& position, const char* message) { + std::string status; + switch (level) { + case SPV_MSG_FATAL: + case SPV_MSG_INTERNAL_ERROR: + case SPV_MSG_ERROR: + status = "ERROR"; + break; + case SPV_MSG_WARNING: + case SPV_MSG_INFO: + case SPV_MSG_DEBUG: + status = "INFO"; + break; + } + *buffer << status << " " << position.line << ":" << position.column << ":" + << position.index << ": " << message << std::endl; + }; +} + +void LogMutatorError(const Mutator& mutator, const std::string& error_dir) { + static uint32_t mutator_count = 0; + auto error_path = error_dir.empty() ? error_dir : error_dir + "/mutator/"; + mutator.LogErrors(error_dir.empty() ? nullptr : &error_path, mutator_count++); +} + +void LogWgslError(const std::string& message, + const uint8_t* data, + size_t size, + const std::string& wgsl, + OutputFormat output_format, + const std::string& error_dir) { + static uint32_t wgsl_count = 0; + std::string error_type; + switch (output_format) { + case OutputFormat::kSpv: + error_type = "WGSL -> SPV"; + break; + case OutputFormat::kMSL: + error_type = "WGSL -> MSL"; + break; + case OutputFormat::kHLSL: + error_type = "WGSL -> HLSL"; + break; + case OutputFormat::kWGSL: + error_type = "WGSL -> WGSL"; + break; + default: + assert(false && "All output formats should've been handled"); + break; + } + auto error_path = error_dir.empty() ? error_dir : error_dir + "/wgsl/"; + LogError(wgsl_count++, error_type, message, + error_dir.empty() ? nullptr : &error_path, data, size, &wgsl); +} + +void LogSpvError(const std::string& message, + const uint8_t* data, + size_t size, + const std::string& error_dir) { + static uint32_t spv_count = 0; + auto error_path = error_dir.empty() ? error_dir : error_dir + "/spv/"; + LogError(spv_count++, "SPV -> WGSL", message, + error_dir.empty() ? nullptr : &error_path, data, size, nullptr); +} + +bool ReadBinary(const std::string& path, std::vector* out) { + if (!out) { + return false; + } + + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { + return false; + } + + auto size = file.tellg(); + if (!file) { + return false; + } + + file.seekg(0); + if (!file) { + return false; + } + + std::vector binary(static_cast(size)); + if (!file.read(binary.data(), size)) { + return false; + } + + out->resize(binary.size() / sizeof(uint32_t)); + std::memcpy(out->data(), binary.data(), binary.size()); + return true; +} + +bool WriteBinary(const std::string& path, const std::vector& binary) { + return WriteBinary(path, reinterpret_cast(binary.data()), + binary.size() * sizeof(uint32_t)); +} + +} // namespace util +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint diff --git a/fuzzers/tint_spirv_tools_fuzzer/util.h b/fuzzers/tint_spirv_tools_fuzzer/util.h new file mode 100644 index 0000000000..b5abe478f6 --- /dev/null +++ b/fuzzers/tint_spirv_tools_fuzzer/util.h @@ -0,0 +1,96 @@ +// Copyright 2021 The Tint Authors. +// +// 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 FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_ +#define FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_ + +#include +#include +#include + +#include "fuzzers/tint_common_fuzzer.h" +#include "fuzzers/tint_spirv_tools_fuzzer/mutator.h" + +#include "spirv-tools/libspirv.hpp" + +namespace tint { +namespace fuzzers { +namespace spvtools_fuzzer { +namespace util { + +/// @param buffer will be used to output errors by the returned message +/// consumer. Must remain in scope as long as the returned consumer is in +/// scope. +/// @return the message consumer that will print errors to the `buffer`. +spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer); + +/// Output errors from the SPV -> WGSL conversion. +/// +/// @param message - the error message. +/// @param data - invalid SPIR-V binary. +/// @param size - the size of `data`. +/// @param error_dir - the directory, to which the binary will be printed to. +/// If it's empty, the invalid binary and supplemental files will not be +/// printed. Otherwise, it must have a `spv/` subdirectory. +void LogSpvError(const std::string& message, + const uint8_t* data, + size_t size, + const std::string& error_dir); + +/// Output errors from the WGSL -> `output_format` conversion. +/// +/// @param message - the error message. +/// @param data - the SPIR-V binary that generated the WGSL binary. +/// @param size - the size of `data`. +/// @param wgsl - the invalid WGSL binary. +/// @param output_format - the format which we attempted to convert `wgsl` to. +/// @param error_dir - the directory, to which the binary will be printed out. +/// If it's empty, the invalid binary and supplemental files will not be +/// printed. Otherwise, it must have a `wgsl/` subdirectory. +void LogWgslError(const std::string& message, + const uint8_t* data, + size_t size, + const std::string& wgsl, + OutputFormat output_format, + const std::string& error_dir); + +/// Output errors produced by the mutator. +/// +/// @param mutator - the mutator with invalid state. +/// @param error_dir - the directory, to which invalid files will be printed to. +/// If it's empty, the invalid binary and supplemental files will not be +/// printed. Otherwise, it must have a `mutator/` subdirectory. +void LogMutatorError(const Mutator& mutator, const std::string& error_dir); + +/// Reads SPIR-V binary from `path` into `out`. Returns `true` if successful and +/// `false` otherwise (in this case, `out` is unchanged). +/// +/// @param path - the path to the SPIR-V binary. +/// @param out - may be a `nullptr`. In this case, `false` is returned. +/// @return `true` if successful and `false` otherwise. +bool ReadBinary(const std::string& path, std::vector* out); + +/// Writes `binary` into `path`. +/// +/// @param path - the path to write `binary` to. +/// @param binary - SPIR-V binary. +/// @return whether the operation was successful. +bool WriteBinary(const std::string& path, const std::vector& binary); + +} // namespace util +} // namespace spvtools_fuzzer +} // namespace fuzzers +} // namespace tint + +#endif // FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_ diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 734ebca931..230d66e7d8 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -17,6 +17,13 @@ if (${TINT_BUILD_TESTS} AND NOT TARGET gmock) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/googletest EXCLUDE_FROM_ALL) endif() +if (${TINT_BUILD_SPIRV_TOOLS_FUZZER} AND (NOT TARGET protobuf::libprotobuf OR NOT TARGET protobuf::protoc)) + set(SPIRV_BUILD_FUZZER ON CACHE BOOL "Build spirv-fuzz") + set(protobuf_BUILD_TESTS OFF CACHE BOOL "Disable protobuf tests") + set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "Do not build protobuf static runtime") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/protobuf/cmake) +endif() + if(${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER}) if (NOT IS_DIRECTORY "${SPIRV-Headers_SOURCE_DIR}") set(SPIRV-Headers_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/spirv-headers CACHE STRING "")