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 explicit Impl(spv_target_env env, uint32_t limit, bool validate)
64 : target_env(env), step_limit(limit), validate_during_replay(validate) {}
65
66 const spv_target_env target_env; // Target environment.
67 MessageConsumer consumer; // Message consumer.
68 const uint32_t step_limit; // Step limit for reductions.
69 const bool validate_during_replay; // Determines whether to check for
70 // validity during the replaying of
71 // transformations.
72 };
73
Shrinker(spv_target_env env,uint32_t step_limit,bool validate_during_replay)74 Shrinker::Shrinker(spv_target_env env, uint32_t step_limit,
75 bool validate_during_replay)
76 : impl_(MakeUnique<Impl>(env, step_limit, validate_during_replay)) {}
77
78 Shrinker::~Shrinker() = default;
79
SetMessageConsumer(MessageConsumer c)80 void Shrinker::SetMessageConsumer(MessageConsumer c) {
81 impl_->consumer = std::move(c);
82 }
83
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) const84 Shrinker::ShrinkerResultStatus Shrinker::Run(
85 const std::vector<uint32_t>& binary_in,
86 const protobufs::FactSequence& initial_facts,
87 const protobufs::TransformationSequence& transformation_sequence_in,
88 const Shrinker::InterestingnessFunction& interestingness_function,
89 std::vector<uint32_t>* binary_out,
90 protobufs::TransformationSequence* transformation_sequence_out) const {
91 // Check compatibility between the library version being linked with and the
92 // header files being used.
93 GOOGLE_PROTOBUF_VERIFY_VERSION;
94
95 spvtools::SpirvTools tools(impl_->target_env);
96 if (!tools.IsValid()) {
97 impl_->consumer(SPV_MSG_ERROR, nullptr, {},
98 "Failed to create SPIRV-Tools interface; stopping.");
99 return Shrinker::ShrinkerResultStatus::kFailedToCreateSpirvToolsInterface;
100 }
101
102 // Initial binary should be valid.
103 if (!tools.Validate(&binary_in[0], binary_in.size())) {
104 impl_->consumer(SPV_MSG_INFO, nullptr, {},
105 "Initial binary is invalid; stopping.");
106 return Shrinker::ShrinkerResultStatus::kInitialBinaryInvalid;
107 }
108
109 std::vector<uint32_t> current_best_binary;
110 protobufs::TransformationSequence current_best_transformations;
111
112 // Run a replay of the initial transformation sequence to (a) check that it
113 // succeeds, (b) get the binary that results from running these
114 // transformations, and (c) get the subsequence of the initial transformations
115 // that actually apply (in principle this could be a strict subsequence).
116 if (Replayer(impl_->target_env, impl_->validate_during_replay)
117 .Run(binary_in, initial_facts, transformation_sequence_in,
118 ¤t_best_binary, ¤t_best_transformations) !=
119 Replayer::ReplayerResultStatus::kComplete) {
120 return ShrinkerResultStatus::kReplayFailed;
121 }
122
123 // Check that the binary produced by applying the initial transformations is
124 // indeed interesting.
125 if (!interestingness_function(current_best_binary, 0)) {
126 impl_->consumer(SPV_MSG_INFO, nullptr, {},
127 "Initial binary is not interesting; stopping.");
128 return ShrinkerResultStatus::kInitialBinaryNotInteresting;
129 }
130
131 uint32_t attempt = 0; // Keeps track of the number of shrink attempts that
132 // have been tried, whether successful or not.
133
134 uint32_t chunk_size =
135 std::max(1u, NumRemainingTransformations(current_best_transformations) /
136 2); // The number of contiguous transformations that the
137 // shrinker will try to remove in one go; starts
138 // high and decreases during the shrinking process.
139
140 // Keep shrinking until we:
141 // - reach the step limit,
142 // - run out of transformations to remove, or
143 // - cannot make the chunk size any smaller.
144 while (attempt < impl_->step_limit &&
145 !current_best_transformations.transformation().empty() &&
146 chunk_size > 0) {
147 bool progress_this_round =
148 false; // Used to decide whether to make the chunk size with which we
149 // remove transformations smaller. If we managed to remove at
150 // least one chunk of transformations at a particular chunk
151 // size, we set this flag so that we do not yet decrease the
152 // chunk size.
153
154 assert(chunk_size <=
155 NumRemainingTransformations(current_best_transformations) &&
156 "Chunk size should never exceed the number of transformations that "
157 "remain.");
158
159 // The number of chunks is the ceiling of (#remaining_transformations /
160 // chunk_size).
161 const uint32_t num_chunks =
162 (NumRemainingTransformations(current_best_transformations) +
163 chunk_size - 1) /
164 chunk_size;
165 assert(num_chunks >= 1 && "There should be at least one chunk.");
166 assert(num_chunks * chunk_size >=
167 NumRemainingTransformations(current_best_transformations) &&
168 "All transformations should be in some chunk.");
169
170 // We go through the transformations in reverse, in chunks of size
171 // |chunk_size|, using |chunk_index| to track which chunk to try removing
172 // next. The loop exits early if we reach the shrinking step limit.
173 for (int chunk_index = num_chunks - 1;
174 attempt < impl_->step_limit && chunk_index >= 0; chunk_index--) {
175 // Remove a chunk of transformations according to the current index and
176 // chunk size.
177 auto transformations_with_chunk_removed =
178 RemoveChunk(current_best_transformations, chunk_index, chunk_size);
179
180 // Replay the smaller sequence of transformations to get a next binary and
181 // transformation sequence. Note that the transformations arising from
182 // replay might be even smaller than the transformations with the chunk
183 // removed, because removing those transformations might make further
184 // transformations inapplicable.
185 std::vector<uint32_t> next_binary;
186 protobufs::TransformationSequence next_transformation_sequence;
187 if (Replayer(impl_->target_env, false)
188 .Run(binary_in, initial_facts, transformations_with_chunk_removed,
189 &next_binary, &next_transformation_sequence) !=
190 Replayer::ReplayerResultStatus::kComplete) {
191 // Replay should not fail; if it does, we need to abort shrinking.
192 return ShrinkerResultStatus::kReplayFailed;
193 }
194
195 assert(NumRemainingTransformations(next_transformation_sequence) >=
196 chunk_index * chunk_size &&
197 "Removing this chunk of transformations should not have an effect "
198 "on earlier chunks.");
199
200 if (interestingness_function(next_binary, attempt)) {
201 // If the binary arising from the smaller transformation sequence is
202 // interesting, this becomes our current best binary and transformation
203 // sequence.
204 current_best_binary = next_binary;
205 current_best_transformations = next_transformation_sequence;
206 progress_this_round = true;
207 }
208 // Either way, this was a shrink attempt, so increment our count of shrink
209 // attempts.
210 attempt++;
211 }
212 if (!progress_this_round) {
213 // If we didn't manage to remove any chunks at this chunk size, try a
214 // smaller chunk size.
215 chunk_size /= 2;
216 }
217 // Decrease the chunk size until it becomes no larger than the number of
218 // remaining transformations.
219 while (chunk_size >
220 NumRemainingTransformations(current_best_transformations)) {
221 chunk_size /= 2;
222 }
223 }
224
225 // The output from the shrinker is the best binary we saw, and the
226 // transformations that led to it.
227 *binary_out = current_best_binary;
228 *transformation_sequence_out = current_best_transformations;
229
230 // Indicate whether shrinking completed or was truncated due to reaching the
231 // step limit.
232 assert(attempt <= impl_->step_limit);
233 if (attempt == impl_->step_limit) {
234 std::stringstream strstream;
235 strstream << "Shrinking did not complete; step limit " << impl_->step_limit
236 << " was reached.";
237 impl_->consumer(SPV_MSG_WARNING, nullptr, {}, strstream.str().c_str());
238 return Shrinker::ShrinkerResultStatus::kStepLimitReached;
239 }
240 return Shrinker::ShrinkerResultStatus::kComplete;
241 }
242
243 } // namespace fuzz
244 } // namespace spvtools
245