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