• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2019 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "source/fuzz/shrinker.h"
16 
17 #include <sstream>
18 
19 #include "source/fuzz/pseudo_random_generator.h"
20 #include "source/fuzz/replayer.h"
21 #include "source/spirv_fuzzer_options.h"
22 #include "source/util/make_unique.h"
23 
24 namespace spvtools {
25 namespace fuzz {
26 
27 namespace {
28 
29 // A helper to get the size of a protobuf transformation sequence in a less
30 // verbose manner.
NumRemainingTransformations(const protobufs::TransformationSequence & transformation_sequence)31 uint32_t NumRemainingTransformations(
32     const protobufs::TransformationSequence& transformation_sequence) {
33   return static_cast<uint32_t>(transformation_sequence.transformation_size());
34 }
35 
36 // A helper to return a transformation sequence identical to |transformations|,
37 // except that a chunk of size |chunk_size| starting from |chunk_index| x
38 // |chunk_size| is removed (or as many transformations as available if the whole
39 // chunk is not).
RemoveChunk(const protobufs::TransformationSequence & transformations,uint32_t chunk_index,uint32_t chunk_size)40 protobufs::TransformationSequence RemoveChunk(
41     const protobufs::TransformationSequence& transformations,
42     uint32_t chunk_index, uint32_t chunk_size) {
43   uint32_t lower = chunk_index * chunk_size;
44   uint32_t upper = std::min((chunk_index + 1) * chunk_size,
45                             NumRemainingTransformations(transformations));
46   assert(lower < upper);
47   assert(upper <= NumRemainingTransformations(transformations));
48   protobufs::TransformationSequence result;
49   for (uint32_t j = 0; j < NumRemainingTransformations(transformations); j++) {
50     if (j >= lower && j < upper) {
51       continue;
52     }
53     protobufs::Transformation transformation =
54         transformations.transformation()[j];
55     *result.mutable_transformation()->Add() = transformation;
56   }
57   return result;
58 }
59 
60 }  // namespace
61 
62 struct Shrinker::Impl {
Implspvtools::fuzz::Shrinker::Impl63   Impl(spv_target_env env, uint32_t limit, bool validate,
64        spv_validator_options options)
65       : target_env(env),
66         step_limit(limit),
67         validate_during_replay(validate),
68         validator_options(options) {}
69 
70   const spv_target_env target_env;          // Target environment.
71   MessageConsumer consumer;                 // Message consumer.
72   const uint32_t step_limit;                // Step limit for reductions.
73   const bool validate_during_replay;        // Determines whether to check for
74                                             // validity during the replaying of
75                                             // transformations.
76   spv_validator_options validator_options;  // Options to control validation.
77 };
78 
Shrinker(spv_target_env env,uint32_t step_limit,bool validate_during_replay,spv_validator_options validator_options)79 Shrinker::Shrinker(spv_target_env env, uint32_t step_limit,
80                    bool validate_during_replay,
81                    spv_validator_options validator_options)
82     : impl_(MakeUnique<Impl>(env, step_limit, validate_during_replay,
83                              validator_options)) {}
84 
85 Shrinker::~Shrinker() = default;
86 
SetMessageConsumer(MessageConsumer c)87 void Shrinker::SetMessageConsumer(MessageConsumer c) {
88   impl_->consumer = std::move(c);
89 }
90 
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) const91 Shrinker::ShrinkerResultStatus Shrinker::Run(
92     const std::vector<uint32_t>& binary_in,
93     const protobufs::FactSequence& initial_facts,
94     const protobufs::TransformationSequence& transformation_sequence_in,
95     const Shrinker::InterestingnessFunction& interestingness_function,
96     std::vector<uint32_t>* binary_out,
97     protobufs::TransformationSequence* transformation_sequence_out) const {
98   // Check compatibility between the library version being linked with and the
99   // header files being used.
100   GOOGLE_PROTOBUF_VERIFY_VERSION;
101 
102   spvtools::SpirvTools tools(impl_->target_env);
103   if (!tools.IsValid()) {
104     impl_->consumer(SPV_MSG_ERROR, nullptr, {},
105                     "Failed to create SPIRV-Tools interface; stopping.");
106     return Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface;
107   }
108 
109   // Initial binary should be valid.
110   if (!tools.Validate(&binary_in[0], binary_in.size())) {
111     impl_->consumer(SPV_MSG_INFO, nullptr, {},
112                     "Initial binary is invalid; stopping.");
113     return Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid;
114   }
115 
116   std::vector<uint32_t> current_best_binary;
117   protobufs::TransformationSequence current_best_transformations;
118 
119   // Run a replay of the initial transformation sequence to (a) check that it
120   // succeeds, (b) get the binary that results from running these
121   // transformations, and (c) get the subsequence of the initial transformations
122   // that actually apply (in principle this could be a strict subsequence).
123   if (Replayer(impl_->target_env, impl_->validate_during_replay,
124                impl_->validator_options)
125           .Run(binary_in, initial_facts, transformation_sequence_in,
126                &current_best_binary, &current_best_transformations) !=
127       Replayer::ReplayerResultStatus::kComplete) {
128     return ShrinkerResultStatus::kReplayFailed;
129   }
130 
131   // Check that the binary produced by applying the initial transformations is
132   // indeed interesting.
133   if (!interestingness_function(current_best_binary, 0)) {
134     impl_->consumer(SPV_MSG_INFO, nullptr, {},
135                     "Initial binary is not interesting; stopping.");
136     return ShrinkerResultStatus::kInitialBinaryNotInteresting;
137   }
138 
139   uint32_t attempt = 0;  // Keeps track of the number of shrink attempts that
140                          // have been tried, whether successful or not.
141 
142   uint32_t chunk_size =
143       std::max(1u, NumRemainingTransformations(current_best_transformations) /
144                        2);  // The number of contiguous transformations that the
145                             // shrinker will try to remove in one go; starts
146                             // high and decreases during the shrinking process.
147 
148   // Keep shrinking until we:
149   // - reach the step limit,
150   // - run out of transformations to remove, or
151   // - cannot make the chunk size any smaller.
152   while (attempt < impl_->step_limit &&
153          !current_best_transformations.transformation().empty() &&
154          chunk_size > 0) {
155     bool progress_this_round =
156         false;  // Used to decide whether to make the chunk size with which we
157                 // remove transformations smaller.  If we managed to remove at
158                 // least one chunk of transformations at a particular chunk
159                 // size, we set this flag so that we do not yet decrease the
160                 // chunk size.
161 
162     assert(chunk_size <=
163                NumRemainingTransformations(current_best_transformations) &&
164            "Chunk size should never exceed the number of transformations that "
165            "remain.");
166 
167     // The number of chunks is the ceiling of (#remaining_transformations /
168     // chunk_size).
169     const uint32_t num_chunks =
170         (NumRemainingTransformations(current_best_transformations) +
171          chunk_size - 1) /
172         chunk_size;
173     assert(num_chunks >= 1 && "There should be at least one chunk.");
174     assert(num_chunks * chunk_size >=
175                NumRemainingTransformations(current_best_transformations) &&
176            "All transformations should be in some chunk.");
177 
178     // We go through the transformations in reverse, in chunks of size
179     // |chunk_size|, using |chunk_index| to track which chunk to try removing
180     // next.  The loop exits early if we reach the shrinking step limit.
181     for (int chunk_index = num_chunks - 1;
182          attempt < impl_->step_limit && chunk_index >= 0; chunk_index--) {
183       // Remove a chunk of transformations according to the current index and
184       // chunk size.
185       auto transformations_with_chunk_removed =
186           RemoveChunk(current_best_transformations, chunk_index, chunk_size);
187 
188       // Replay the smaller sequence of transformations to get a next binary and
189       // transformation sequence. Note that the transformations arising from
190       // replay might be even smaller than the transformations with the chunk
191       // removed, because removing those transformations might make further
192       // transformations inapplicable.
193       std::vector<uint32_t> next_binary;
194       protobufs::TransformationSequence next_transformation_sequence;
195       if (Replayer(impl_->target_env, impl_->validate_during_replay,
196                    impl_->validator_options)
197               .Run(binary_in, initial_facts, transformations_with_chunk_removed,
198                    &next_binary, &next_transformation_sequence) !=
199           Replayer::ReplayerResultStatus::kComplete) {
200         // Replay should not fail; if it does, we need to abort shrinking.
201         return ShrinkerResultStatus::kReplayFailed;
202       }
203 
204       assert(NumRemainingTransformations(next_transformation_sequence) >=
205                  chunk_index * chunk_size &&
206              "Removing this chunk of transformations should not have an effect "
207              "on earlier chunks.");
208 
209       if (interestingness_function(next_binary, attempt)) {
210         // If the binary arising from the smaller transformation sequence is
211         // interesting, this becomes our current best binary and transformation
212         // sequence.
213         current_best_binary = next_binary;
214         current_best_transformations = next_transformation_sequence;
215         progress_this_round = true;
216       }
217       // Either way, this was a shrink attempt, so increment our count of shrink
218       // attempts.
219       attempt++;
220     }
221     if (!progress_this_round) {
222       // If we didn't manage to remove any chunks at this chunk size, try a
223       // smaller chunk size.
224       chunk_size /= 2;
225     }
226     // Decrease the chunk size until it becomes no larger than the number of
227     // remaining transformations.
228     while (chunk_size >
229            NumRemainingTransformations(current_best_transformations)) {
230       chunk_size /= 2;
231     }
232   }
233 
234   // The output from the shrinker is the best binary we saw, and the
235   // transformations that led to it.
236   *binary_out = current_best_binary;
237   *transformation_sequence_out = current_best_transformations;
238 
239   // Indicate whether shrinking completed or was truncated due to reaching the
240   // step limit.
241   assert(attempt <= impl_->step_limit);
242   if (attempt == impl_->step_limit) {
243     std::stringstream strstream;
244     strstream << "Shrinking did not complete; step limit " << impl_->step_limit
245               << " was reached.";
246     impl_->consumer(SPV_MSG_WARNING, nullptr, {}, strstream.str().c_str());
247     return Shrinker::ShrinkerResultStatus::kStepLimitReached;
248   }
249   return Shrinker::ShrinkerResultStatus::kComplete;
250 }
251 
252 }  // namespace fuzz
253 }  // namespace spvtools
254