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 <cassert>
16 #include <cerrno>
17 #include <cstring>
18 #include <fstream>
19 #include <memory>
20 #include <random>
21 #include <sstream>
22 #include <string>
23
24 #include "source/fuzz/force_render_red.h"
25 #include "source/fuzz/fuzzer.h"
26 #include "source/fuzz/fuzzer_util.h"
27 #include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
28 #include "source/fuzz/pseudo_random_generator.h"
29 #include "source/fuzz/replayer.h"
30 #include "source/fuzz/shrinker.h"
31 #include "source/opt/build_module.h"
32 #include "source/opt/ir_context.h"
33 #include "source/opt/log.h"
34 #include "source/spirv_fuzzer_options.h"
35 #include "source/util/make_unique.h"
36 #include "source/util/string_utils.h"
37 #include "tools/io.h"
38 #include "tools/util/cli_consumer.h"
39
40 namespace {
41
42 enum class FuzzingTarget { kSpirv, kWgsl };
43
44 // Check that the std::system function can actually be used.
CheckExecuteCommand()45 bool CheckExecuteCommand() {
46 int res = std::system(nullptr);
47 return res != 0;
48 }
49
50 // Execute a command using the shell.
51 // Returns true if and only if the command's exit status was 0.
ExecuteCommand(const std::string & command)52 bool ExecuteCommand(const std::string& command) {
53 errno = 0;
54 int status = std::system(command.c_str());
55 assert(errno == 0 && "failed to execute command");
56 // The result returned by 'system' is implementation-defined, but is
57 // usually the case that the returned value is 0 when the command's exit
58 // code was 0. We are assuming that here, and that's all we depend on.
59 return status == 0;
60 }
61
62 // Status and actions to perform after parsing command-line arguments.
63 enum class FuzzActions {
64 FORCE_RENDER_RED, // Turn the shader into a form such that it is guaranteed
65 // to render a red image.
66 FUZZ, // Run the fuzzer to apply transformations in a randomized fashion.
67 REPLAY, // Replay an existing sequence of transformations.
68 SHRINK, // Shrink an existing sequence of transformations with respect to an
69 // interestingness function.
70 STOP // Do nothing.
71 };
72
73 struct FuzzStatus {
74 FuzzActions action;
75 int code;
76 };
77
PrintUsage(const char * program)78 void PrintUsage(const char* program) {
79 // NOTE: Please maintain flags in lexicographical order.
80 printf(
81 R"(%s - Fuzzes an equivalent SPIR-V binary based on a given binary.
82
83 USAGE: %s [options] <input.spv> -o <output.spv> \
84 --donors=<donors.txt>
85 USAGE: %s [options] <input.spv> -o <output.spv> \
86 --shrink=<input.transformations> -- <interestingness_test> [args...]
87
88 The SPIR-V binary is read from <input.spv>. If <input.facts> is also present,
89 facts about the SPIR-V binary are read from this file.
90
91 The transformed SPIR-V binary is written to <output.spv>. Human-readable and
92 binary representations of the transformations that were applied are written to
93 <output.transformations_json> and <output.transformations>, respectively.
94
95 When passing --shrink=<input.transformations> an <interestingness_test>
96 must also be provided; this is the path to a script that returns 0 if and only
97 if a given SPIR-V binary is interesting. The SPIR-V binary will be passed to
98 the script as an argument after any other provided arguments [args...]. The
99 "--" characters are optional but denote that all arguments that follow are
100 positional arguments and thus will be forwarded to the interestingness script,
101 and not parsed by %s.
102
103 NOTE: The fuzzer is a work in progress.
104
105 Options (in lexicographical order):
106
107 -h, --help
108 Print this help.
109 --donors=
110 File specifying a series of donor files, one per line. Must be
111 provided if the tool is invoked in fuzzing mode; incompatible
112 with replay and shrink modes. The file should be empty if no
113 donors are to be used.
114 --enable-all-passes
115 By default, spirv-fuzz follows the philosophy of "swarm testing"
116 (Groce et al., 2012): only a subset of fuzzer passes are enabled
117 on any given fuzzer run, with the subset being chosen randomly.
118 This flag instead forces *all* fuzzer passes to be enabled. When
119 running spirv-fuzz many times this is likely to produce *less*
120 diverse fuzzed modules than when swarm testing is used. The
121 purpose of the flag is to allow that hypothesis to be tested.
122 --force-render-red
123 Transforms the input shader into a shader that writes red to the
124 output buffer, and then captures the original shader as the body
125 of a conditional with a dynamically false guard. Exploits input
126 facts to make the guard non-obviously false. This option is a
127 helper for massaging crash-inducing tests into a runnable
128 format; it does not perform any fuzzing.
129 --fuzzer-pass-validation
130 Run the validator after applying each fuzzer pass during
131 fuzzing. Aborts fuzzing early if an invalid binary is created.
132 Useful for debugging spirv-fuzz.
133 --repeated-pass-strategy=
134 Available strategies are:
135 - looped (the default): a sequence of fuzzer passes is chosen at
136 the start of fuzzing, via randomly choosing enabled passes, and
137 augmenting these choices with fuzzer passes that it is
138 recommended to run subsequently. Fuzzing then involves
139 repeatedly applying this fixed sequence of passes.
140 - random: each time a fuzzer pass is requested, this strategy
141 either provides one at random from the set of enabled passes,
142 or provides a pass that has been recommended based on a pass
143 that was used previously.
144 - simple: each time a fuzzer pass is requested, one is provided
145 at random from the set of enabled passes.
146 --fuzzing-target=
147 This option will adjust probabilities of applying certain
148 transformations s.t. the module always remains valid according
149 to the semantics of some fuzzing target. Available targets:
150 - spir-v - module is valid according to the SPIR-V spec.
151 - wgsl - module is valid according to the WGSL spec.
152 --replay
153 File from which to read a sequence of transformations to replay
154 (instead of fuzzing)
155 --replay-range=
156 Signed 32-bit integer. If set to a positive value N, only the
157 first N transformations will be applied during replay. If set to
158 a negative value -N, all but the final N transformations will be
159 applied during replay. If set to 0 (the default), all
160 transformations will be applied during replay. Ignored unless
161 --replay is used.
162 --replay-validation
163 Run the validator after applying each transformation during
164 replay (including the replay that occurs during shrinking).
165 Aborts if an invalid binary is created. Useful for debugging
166 spirv-fuzz.
167 --seed=
168 Unsigned 32-bit integer seed to control random number
169 generation.
170 --shrink=
171 File from which to read a sequence of transformations to shrink
172 (instead of fuzzing)
173 --shrinker-step-limit=
174 Unsigned 32-bit integer specifying maximum number of steps the
175 shrinker will take before giving up. Ignored unless --shrink
176 is used.
177 --shrinker-temp-file-prefix=
178 Specifies a temporary file prefix that will be used to output
179 temporary shader files during shrinking. A number and .spv
180 extension will be added. The default is "temp_", which will
181 cause files like "temp_0001.spv" to be output to the current
182 directory. Ignored unless --shrink is used.
183 --version
184 Display fuzzer version information.
185
186 Supported validator options are as follows. See `spirv-val --help` for details.
187 --before-hlsl-legalization
188 --relax-block-layout
189 --relax-logical-pointer
190 --relax-struct-store
191 --scalar-block-layout
192 --skip-block-layout
193 )",
194 program, program, program, program);
195 }
196
197 // Message consumer for this tool. Used to emit diagnostics during
198 // initialization and setup. Note that |source| and |position| are irrelevant
199 // here because we are still not processing a SPIR-V input file.
FuzzDiagnostic(spv_message_level_t level,const char *,const spv_position_t &,const char * message)200 void FuzzDiagnostic(spv_message_level_t level, const char* /*source*/,
201 const spv_position_t& /*position*/, const char* message) {
202 if (level == SPV_MSG_ERROR) {
203 fprintf(stderr, "error: ");
204 }
205 fprintf(stderr, "%s\n", message);
206 }
207
ParseFlags(int argc,const char ** argv,std::string * in_binary_file,std::string * out_binary_file,std::string * donors_file,std::string * replay_transformations_file,std::vector<std::string> * interestingness_test,std::string * shrink_transformations_file,std::string * shrink_temp_file_prefix,spvtools::fuzz::RepeatedPassStrategy * repeated_pass_strategy,FuzzingTarget * fuzzing_target,spvtools::FuzzerOptions * fuzzer_options,spvtools::ValidatorOptions * validator_options)208 FuzzStatus ParseFlags(
209 int argc, const char** argv, std::string* in_binary_file,
210 std::string* out_binary_file, std::string* donors_file,
211 std::string* replay_transformations_file,
212 std::vector<std::string>* interestingness_test,
213 std::string* shrink_transformations_file,
214 std::string* shrink_temp_file_prefix,
215 spvtools::fuzz::RepeatedPassStrategy* repeated_pass_strategy,
216 FuzzingTarget* fuzzing_target, spvtools::FuzzerOptions* fuzzer_options,
217 spvtools::ValidatorOptions* validator_options) {
218 uint32_t positional_arg_index = 0;
219 bool only_positional_arguments_remain = false;
220 bool force_render_red = false;
221
222 *repeated_pass_strategy =
223 spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations;
224
225 for (int argi = 1; argi < argc; ++argi) {
226 const char* cur_arg = argv[argi];
227 if ('-' == cur_arg[0] && !only_positional_arguments_remain) {
228 if (0 == strcmp(cur_arg, "--version")) {
229 spvtools::Logf(FuzzDiagnostic, SPV_MSG_INFO, nullptr, {}, "%s\n",
230 spvSoftwareVersionDetailsString());
231 return {FuzzActions::STOP, 0};
232 } else if (0 == strcmp(cur_arg, "--help") || 0 == strcmp(cur_arg, "-h")) {
233 PrintUsage(argv[0]);
234 return {FuzzActions::STOP, 0};
235 } else if (0 == strcmp(cur_arg, "-o")) {
236 if (out_binary_file->empty() && argi + 1 < argc) {
237 *out_binary_file = std::string(argv[++argi]);
238 } else {
239 PrintUsage(argv[0]);
240 return {FuzzActions::STOP, 1};
241 }
242 } else if (0 == strncmp(cur_arg, "--donors=", sizeof("--donors=") - 1)) {
243 const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
244 *donors_file = std::string(split_flag.second);
245 } else if (0 == strncmp(cur_arg, "--enable-all-passes",
246 sizeof("--enable-all-passes") - 1)) {
247 fuzzer_options->enable_all_passes();
248 } else if (0 == strncmp(cur_arg, "--force-render-red",
249 sizeof("--force-render-red") - 1)) {
250 force_render_red = true;
251 } else if (0 == strncmp(cur_arg, "--fuzzer-pass-validation",
252 sizeof("--fuzzer-pass-validation") - 1)) {
253 fuzzer_options->enable_fuzzer_pass_validation();
254 } else if (0 == strncmp(cur_arg, "--replay=", sizeof("--replay=") - 1)) {
255 const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
256 *replay_transformations_file = std::string(split_flag.second);
257 } else if (0 == strncmp(cur_arg, "--repeated-pass-strategy=",
258 sizeof("--repeated-pass-strategy=") - 1)) {
259 std::string strategy = spvtools::utils::SplitFlagArgs(cur_arg).second;
260 if (strategy == "looped") {
261 *repeated_pass_strategy =
262 spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations;
263 } else if (strategy == "random") {
264 *repeated_pass_strategy =
265 spvtools::fuzz::RepeatedPassStrategy::kRandomWithRecommendations;
266 } else if (strategy == "simple") {
267 *repeated_pass_strategy =
268 spvtools::fuzz::RepeatedPassStrategy::kSimple;
269 } else {
270 std::stringstream ss;
271 ss << "Unknown repeated pass strategy '" << strategy << "'"
272 << std::endl;
273 ss << "Valid options are 'looped', 'random' and 'simple'.";
274 spvtools::Error(FuzzDiagnostic, nullptr, {}, ss.str().c_str());
275 return {FuzzActions::STOP, 1};
276 }
277 } else if (0 == strncmp(cur_arg, "--fuzzing-target=",
278 sizeof("--fuzzing-target=") - 1)) {
279 std::string target = spvtools::utils::SplitFlagArgs(cur_arg).second;
280 if (target == "spir-v") {
281 *fuzzing_target = FuzzingTarget::kSpirv;
282 } else if (target == "wgsl") {
283 *fuzzing_target = FuzzingTarget::kWgsl;
284 } else {
285 std::stringstream ss;
286 ss << "Unknown fuzzing target '" << target << "'" << std::endl;
287 ss << "Valid options are 'spir-v' and 'wgsl'.";
288 spvtools::Error(FuzzDiagnostic, nullptr, {}, ss.str().c_str());
289 return {FuzzActions::STOP, 1};
290 }
291 } else if (0 == strncmp(cur_arg, "--replay-range=",
292 sizeof("--replay-range=") - 1)) {
293 const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
294 char* end = nullptr;
295 errno = 0;
296 const auto replay_range =
297 static_cast<int32_t>(strtol(split_flag.second.c_str(), &end, 10));
298 assert(end != split_flag.second.c_str() && errno == 0);
299 fuzzer_options->set_replay_range(replay_range);
300 } else if (0 == strncmp(cur_arg, "--replay-validation",
301 sizeof("--replay-validation") - 1)) {
302 fuzzer_options->enable_replay_validation();
303 } else if (0 == strncmp(cur_arg, "--shrink=", sizeof("--shrink=") - 1)) {
304 const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
305 *shrink_transformations_file = std::string(split_flag.second);
306 } else if (0 == strncmp(cur_arg, "--seed=", sizeof("--seed=") - 1)) {
307 const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
308 char* end = nullptr;
309 errno = 0;
310 const auto seed =
311 static_cast<uint32_t>(strtol(split_flag.second.c_str(), &end, 10));
312 assert(end != split_flag.second.c_str() && errno == 0);
313 fuzzer_options->set_random_seed(seed);
314 } else if (0 == strncmp(cur_arg, "--shrinker-step-limit=",
315 sizeof("--shrinker-step-limit=") - 1)) {
316 const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
317 char* end = nullptr;
318 errno = 0;
319 const auto step_limit =
320 static_cast<uint32_t>(strtol(split_flag.second.c_str(), &end, 10));
321 assert(end != split_flag.second.c_str() && errno == 0);
322 fuzzer_options->set_shrinker_step_limit(step_limit);
323 } else if (0 == strncmp(cur_arg, "--shrinker-temp-file-prefix=",
324 sizeof("--shrinker-temp-file-prefix=") - 1)) {
325 const auto split_flag = spvtools::utils::SplitFlagArgs(cur_arg);
326 *shrink_temp_file_prefix = std::string(split_flag.second);
327 } else if (0 == strcmp(cur_arg, "--before-hlsl-legalization")) {
328 validator_options->SetBeforeHlslLegalization(true);
329 } else if (0 == strcmp(cur_arg, "--relax-logical-pointer")) {
330 validator_options->SetRelaxLogicalPointer(true);
331 } else if (0 == strcmp(cur_arg, "--relax-block-layout")) {
332 validator_options->SetRelaxBlockLayout(true);
333 } else if (0 == strcmp(cur_arg, "--scalar-block-layout")) {
334 validator_options->SetScalarBlockLayout(true);
335 } else if (0 == strcmp(cur_arg, "--skip-block-layout")) {
336 validator_options->SetSkipBlockLayout(true);
337 } else if (0 == strcmp(cur_arg, "--relax-struct-store")) {
338 validator_options->SetRelaxStructStore(true);
339 } else if (0 == strcmp(cur_arg, "--")) {
340 only_positional_arguments_remain = true;
341 } else {
342 std::stringstream ss;
343 ss << "Unrecognized argument: " << cur_arg << std::endl;
344 spvtools::Error(FuzzDiagnostic, nullptr, {}, ss.str().c_str());
345 PrintUsage(argv[0]);
346 return {FuzzActions::STOP, 1};
347 }
348 } else if (positional_arg_index == 0) {
349 // Binary input file name
350 assert(in_binary_file->empty());
351 *in_binary_file = std::string(cur_arg);
352 positional_arg_index++;
353 } else {
354 interestingness_test->push_back(std::string(cur_arg));
355 }
356 }
357
358 if (in_binary_file->empty()) {
359 spvtools::Error(FuzzDiagnostic, nullptr, {}, "No input file specified");
360 return {FuzzActions::STOP, 1};
361 }
362
363 if (out_binary_file->empty()) {
364 spvtools::Error(FuzzDiagnostic, nullptr, {}, "-o required");
365 return {FuzzActions::STOP, 1};
366 }
367
368 auto const_fuzzer_options =
369 static_cast<spv_const_fuzzer_options>(*fuzzer_options);
370 if (force_render_red) {
371 if (!replay_transformations_file->empty() ||
372 !shrink_transformations_file->empty() ||
373 const_fuzzer_options->replay_validation_enabled) {
374 spvtools::Error(FuzzDiagnostic, nullptr, {},
375 "The --force-render-red argument cannot be used with any "
376 "other arguments except -o.");
377 return {FuzzActions::STOP, 1};
378 }
379 return {FuzzActions::FORCE_RENDER_RED, 0};
380 }
381
382 if (replay_transformations_file->empty() &&
383 shrink_transformations_file->empty() &&
384 static_cast<spv_const_fuzzer_options>(*fuzzer_options)
385 ->replay_validation_enabled) {
386 spvtools::Error(FuzzDiagnostic, nullptr, {},
387 "The --replay-validation argument can only be used with "
388 "one of the --replay or --shrink arguments.");
389 return {FuzzActions::STOP, 1};
390 }
391
392 if (shrink_transformations_file->empty() && !interestingness_test->empty()) {
393 spvtools::Error(FuzzDiagnostic, nullptr, {},
394 "Too many positional arguments specified; extra positional "
395 "arguments are used as the interestingness function, which "
396 "are only valid with the --shrink option.");
397 return {FuzzActions::STOP, 1};
398 }
399
400 if (!shrink_transformations_file->empty() && interestingness_test->empty()) {
401 spvtools::Error(
402 FuzzDiagnostic, nullptr, {},
403 "The --shrink option requires an interestingness function.");
404 return {FuzzActions::STOP, 1};
405 }
406
407 if (!replay_transformations_file->empty() ||
408 !shrink_transformations_file->empty()) {
409 // Donors should not be provided when replaying or shrinking: they only make
410 // sense during fuzzing.
411 if (!donors_file->empty()) {
412 spvtools::Error(FuzzDiagnostic, nullptr, {},
413 "The --donors argument is not compatible with --replay "
414 "nor --shrink.");
415 return {FuzzActions::STOP, 1};
416 }
417 }
418
419 if (!replay_transformations_file->empty()) {
420 // A replay transformations file was given, thus the tool is being invoked
421 // in replay mode.
422 if (!shrink_transformations_file->empty()) {
423 spvtools::Error(
424 FuzzDiagnostic, nullptr, {},
425 "The --replay and --shrink arguments are mutually exclusive.");
426 return {FuzzActions::STOP, 1};
427 }
428 return {FuzzActions::REPLAY, 0};
429 }
430
431 if (!shrink_transformations_file->empty()) {
432 // The tool is being invoked in shrink mode.
433 assert(!interestingness_test->empty() &&
434 "An error should have been raised if --shrink was provided without "
435 "an interestingness test.");
436 return {FuzzActions::SHRINK, 0};
437 }
438
439 // The tool is being invoked in fuzz mode.
440 if (donors_file->empty()) {
441 spvtools::Error(FuzzDiagnostic, nullptr, {},
442 "Fuzzing requires that the --donors option is used.");
443 return {FuzzActions::STOP, 1};
444 }
445 return {FuzzActions::FUZZ, 0};
446 }
447
ParseTransformations(const std::string & transformations_file,spvtools::fuzz::protobufs::TransformationSequence * transformations)448 bool ParseTransformations(
449 const std::string& transformations_file,
450 spvtools::fuzz::protobufs::TransformationSequence* transformations) {
451 std::ifstream transformations_stream;
452 transformations_stream.open(transformations_file,
453 std::ios::in | std::ios::binary);
454 auto parse_success =
455 transformations->ParseFromIstream(&transformations_stream);
456 transformations_stream.close();
457 if (!parse_success) {
458 spvtools::Error(FuzzDiagnostic, nullptr, {},
459 ("Error reading transformations from file '" +
460 transformations_file + "'")
461 .c_str());
462 return false;
463 }
464 return true;
465 }
466
Replay(const spv_target_env & target_env,spv_const_fuzzer_options fuzzer_options,spv_validator_options validator_options,const std::vector<uint32_t> & binary_in,const spvtools::fuzz::protobufs::FactSequence & initial_facts,const std::string & replay_transformations_file,std::vector<uint32_t> * binary_out,spvtools::fuzz::protobufs::TransformationSequence * transformations_applied)467 bool Replay(const spv_target_env& target_env,
468 spv_const_fuzzer_options fuzzer_options,
469 spv_validator_options validator_options,
470 const std::vector<uint32_t>& binary_in,
471 const spvtools::fuzz::protobufs::FactSequence& initial_facts,
472 const std::string& replay_transformations_file,
473 std::vector<uint32_t>* binary_out,
474 spvtools::fuzz::protobufs::TransformationSequence*
475 transformations_applied) {
476 spvtools::fuzz::protobufs::TransformationSequence transformation_sequence;
477 if (!ParseTransformations(replay_transformations_file,
478 &transformation_sequence)) {
479 return false;
480 }
481
482 uint32_t num_transformations_to_apply;
483 if (fuzzer_options->replay_range > 0) {
484 // We have a positive replay range, N. We would like transformations
485 // [0, N), truncated to the number of available transformations if N is too
486 // large.
487 num_transformations_to_apply = static_cast<uint32_t>(
488 std::min(fuzzer_options->replay_range,
489 transformation_sequence.transformation_size()));
490 } else {
491 // We have non-positive replay range, -N (where N may be 0). We would like
492 // transformations [0, num_transformations - N), or no transformations if N
493 // is too large.
494 num_transformations_to_apply = static_cast<uint32_t>(
495 std::max(0, transformation_sequence.transformation_size() +
496 fuzzer_options->replay_range));
497 }
498
499 auto replay_result =
500 spvtools::fuzz::Replayer(
501 target_env, spvtools::utils::CLIMessageConsumer, binary_in,
502 initial_facts, transformation_sequence, num_transformations_to_apply,
503 fuzzer_options->replay_validation_enabled, validator_options)
504 .Run();
505 replay_result.transformed_module->module()->ToBinary(binary_out, false);
506 *transformations_applied = std::move(replay_result.applied_transformations);
507 return replay_result.status ==
508 spvtools::fuzz::Replayer::ReplayerResultStatus::kComplete;
509 }
510
Shrink(const spv_target_env & target_env,spv_const_fuzzer_options fuzzer_options,spv_validator_options validator_options,const std::vector<uint32_t> & binary_in,const spvtools::fuzz::protobufs::FactSequence & initial_facts,const std::string & shrink_transformations_file,const std::string & shrink_temp_file_prefix,const std::vector<std::string> & interestingness_command,std::vector<uint32_t> * binary_out,spvtools::fuzz::protobufs::TransformationSequence * transformations_applied)511 bool Shrink(const spv_target_env& target_env,
512 spv_const_fuzzer_options fuzzer_options,
513 spv_validator_options validator_options,
514 const std::vector<uint32_t>& binary_in,
515 const spvtools::fuzz::protobufs::FactSequence& initial_facts,
516 const std::string& shrink_transformations_file,
517 const std::string& shrink_temp_file_prefix,
518 const std::vector<std::string>& interestingness_command,
519 std::vector<uint32_t>* binary_out,
520 spvtools::fuzz::protobufs::TransformationSequence*
521 transformations_applied) {
522 spvtools::fuzz::protobufs::TransformationSequence transformation_sequence;
523 if (!ParseTransformations(shrink_transformations_file,
524 &transformation_sequence)) {
525 return false;
526 }
527 assert(!interestingness_command.empty() &&
528 "An error should have been raised because the interestingness_command "
529 "is empty.");
530 std::stringstream joined;
531 joined << interestingness_command[0];
532 for (size_t i = 1, size = interestingness_command.size(); i < size; ++i) {
533 joined << " " << interestingness_command[i];
534 }
535 std::string interestingness_command_joined = joined.str();
536
537 spvtools::fuzz::Shrinker::InterestingnessFunction interestingness_function =
538 [interestingness_command_joined, shrink_temp_file_prefix](
539 std::vector<uint32_t> binary, uint32_t reductions_applied) -> bool {
540 std::stringstream ss;
541 ss << shrink_temp_file_prefix << std::setw(4) << std::setfill('0')
542 << reductions_applied << ".spv";
543 const auto spv_file = ss.str();
544 const std::string command = interestingness_command_joined + " " + spv_file;
545 auto write_file_succeeded =
546 WriteFile(spv_file.c_str(), "wb", &binary[0], binary.size());
547 (void)(write_file_succeeded);
548 assert(write_file_succeeded);
549 return ExecuteCommand(command);
550 };
551
552 auto shrink_result =
553 spvtools::fuzz::Shrinker(
554 target_env, spvtools::utils::CLIMessageConsumer, binary_in,
555 initial_facts, transformation_sequence, interestingness_function,
556 fuzzer_options->shrinker_step_limit,
557 fuzzer_options->replay_validation_enabled, validator_options)
558 .Run();
559
560 *binary_out = std::move(shrink_result.transformed_binary);
561 *transformations_applied = std::move(shrink_result.applied_transformations);
562 return spvtools::fuzz::Shrinker::ShrinkerResultStatus::kComplete ==
563 shrink_result.status ||
564 spvtools::fuzz::Shrinker::ShrinkerResultStatus::kStepLimitReached ==
565 shrink_result.status;
566 }
567
Fuzz(const spv_target_env & target_env,spv_const_fuzzer_options fuzzer_options,spv_validator_options validator_options,const std::vector<uint32_t> & binary_in,const spvtools::fuzz::protobufs::FactSequence & initial_facts,const std::string & donors,spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,FuzzingTarget fuzzing_target,std::vector<uint32_t> * binary_out,spvtools::fuzz::protobufs::TransformationSequence * transformations_applied)568 bool Fuzz(const spv_target_env& target_env,
569 spv_const_fuzzer_options fuzzer_options,
570 spv_validator_options validator_options,
571 const std::vector<uint32_t>& binary_in,
572 const spvtools::fuzz::protobufs::FactSequence& initial_facts,
573 const std::string& donors,
574 spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
575 FuzzingTarget fuzzing_target, std::vector<uint32_t>* binary_out,
576 spvtools::fuzz::protobufs::TransformationSequence*
577 transformations_applied) {
578 auto message_consumer = spvtools::utils::CLIMessageConsumer;
579
580 std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> donor_suppliers;
581
582 std::ifstream donors_file(donors);
583 if (!donors_file) {
584 spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error opening donors file");
585 return false;
586 }
587 std::string donor_filename;
588 while (std::getline(donors_file, donor_filename)) {
589 donor_suppliers.emplace_back(
590 [donor_filename, message_consumer,
591 target_env]() -> std::unique_ptr<spvtools::opt::IRContext> {
592 std::vector<uint32_t> donor_binary;
593 if (!ReadBinaryFile<uint32_t>(donor_filename.c_str(),
594 &donor_binary)) {
595 return nullptr;
596 }
597 return spvtools::BuildModule(target_env, message_consumer,
598 donor_binary.data(),
599 donor_binary.size());
600 });
601 }
602
603 std::unique_ptr<spvtools::opt::IRContext> ir_context;
604 if (!spvtools::fuzz::fuzzerutil::BuildIRContext(target_env, message_consumer,
605 binary_in, validator_options,
606 &ir_context)) {
607 spvtools::Error(FuzzDiagnostic, nullptr, {}, "Initial binary is invalid");
608 return false;
609 }
610
611 assert((fuzzing_target == FuzzingTarget::kWgsl ||
612 fuzzing_target == FuzzingTarget::kSpirv) &&
613 "Not all fuzzing targets are handled");
614 auto fuzzer_context = spvtools::MakeUnique<spvtools::fuzz::FuzzerContext>(
615 spvtools::MakeUnique<spvtools::fuzz::PseudoRandomGenerator>(
616 fuzzer_options->has_random_seed
617 ? fuzzer_options->random_seed
618 : static_cast<uint32_t>(std::random_device()())),
619 spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()),
620 fuzzing_target == FuzzingTarget::kWgsl);
621
622 auto transformation_context =
623 spvtools::MakeUnique<spvtools::fuzz::TransformationContext>(
624 spvtools::MakeUnique<spvtools::fuzz::FactManager>(ir_context.get()),
625 validator_options);
626 transformation_context->GetFactManager()->AddInitialFacts(message_consumer,
627 initial_facts);
628
629 spvtools::fuzz::Fuzzer fuzzer(
630 std::move(ir_context), std::move(transformation_context),
631 std::move(fuzzer_context), message_consumer, donor_suppliers,
632 fuzzer_options->all_passes_enabled, repeated_pass_strategy,
633 fuzzer_options->fuzzer_pass_validation_enabled, validator_options, false);
634 auto fuzz_result = fuzzer.Run(0);
635 if (fuzz_result.status ==
636 spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule) {
637 spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error running fuzzer");
638 return false;
639 }
640
641 fuzzer.GetIRContext()->module()->ToBinary(binary_out, true);
642 *transformations_applied = fuzzer.GetTransformationSequence();
643 return true;
644 }
645
646 } // namespace
647
648 // Dumps |binary| to file |filename|. Useful for interactive debugging.
DumpShader(const std::vector<uint32_t> & binary,const char * filename)649 void DumpShader(const std::vector<uint32_t>& binary, const char* filename) {
650 auto write_file_succeeded =
651 WriteFile(filename, "wb", &binary[0], binary.size());
652 if (!write_file_succeeded) {
653 std::cerr << "Failed to dump shader" << std::endl;
654 }
655 }
656
657 // Dumps the SPIRV-V module in |context| to file |filename|. Useful for
658 // interactive debugging.
DumpShader(spvtools::opt::IRContext * context,const char * filename)659 void DumpShader(spvtools::opt::IRContext* context, const char* filename) {
660 std::vector<uint32_t> binary;
661 context->module()->ToBinary(&binary, false);
662 DumpShader(binary, filename);
663 }
664
665 // Dumps |transformations| to file |filename| in binary format. Useful for
666 // interactive debugging.
DumpTransformationsBinary(const spvtools::fuzz::protobufs::TransformationSequence & transformations,const char * filename)667 void DumpTransformationsBinary(
668 const spvtools::fuzz::protobufs::TransformationSequence& transformations,
669 const char* filename) {
670 std::ofstream transformations_file;
671 transformations_file.open(filename, std::ios::out | std::ios::binary);
672 transformations.SerializeToOstream(&transformations_file);
673 transformations_file.close();
674 }
675
676 // Dumps |transformations| to file |filename| in JSON format. Useful for
677 // interactive debugging.
DumpTransformationsJson(const spvtools::fuzz::protobufs::TransformationSequence & transformations,const char * filename)678 void DumpTransformationsJson(
679 const spvtools::fuzz::protobufs::TransformationSequence& transformations,
680 const char* filename) {
681 std::string json_string;
682 auto json_options = google::protobuf::util::JsonOptions();
683 json_options.add_whitespace = true;
684 auto json_generation_status = google::protobuf::util::MessageToJsonString(
685 transformations, &json_string, json_options);
686 if (json_generation_status.ok()) {
687 std::ofstream transformations_json_file(filename);
688 transformations_json_file << json_string;
689 transformations_json_file.close();
690 }
691 }
692
693 const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
694
main(int argc,const char ** argv)695 int main(int argc, const char** argv) {
696 std::string in_binary_file;
697 std::string out_binary_file;
698 std::string donors_file;
699 std::string replay_transformations_file;
700 std::vector<std::string> interestingness_test;
701 std::string shrink_transformations_file;
702 std::string shrink_temp_file_prefix = "temp_";
703 spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy;
704 auto fuzzing_target = FuzzingTarget::kSpirv;
705
706 spvtools::FuzzerOptions fuzzer_options;
707 spvtools::ValidatorOptions validator_options;
708
709 FuzzStatus status =
710 ParseFlags(argc, argv, &in_binary_file, &out_binary_file, &donors_file,
711 &replay_transformations_file, &interestingness_test,
712 &shrink_transformations_file, &shrink_temp_file_prefix,
713 &repeated_pass_strategy, &fuzzing_target, &fuzzer_options,
714 &validator_options);
715
716 if (status.action == FuzzActions::STOP) {
717 return status.code;
718 }
719
720 std::vector<uint32_t> binary_in;
721 if (!ReadBinaryFile<uint32_t>(in_binary_file.c_str(), &binary_in)) {
722 return 1;
723 }
724
725 spvtools::fuzz::protobufs::FactSequence initial_facts;
726
727 // If not found, dot_pos will be std::string::npos, which can be used in
728 // substr to mean "the end of the string"; there is no need to check the
729 // result.
730 size_t dot_pos = in_binary_file.rfind('.');
731 std::string in_facts_file = in_binary_file.substr(0, dot_pos) + ".facts";
732 std::ifstream facts_input(in_facts_file);
733 if (facts_input) {
734 std::string facts_json_string((std::istreambuf_iterator<char>(facts_input)),
735 std::istreambuf_iterator<char>());
736 facts_input.close();
737 if (!google::protobuf::util::JsonStringToMessage(facts_json_string,
738 &initial_facts)
739 .ok()) {
740 spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error reading facts data");
741 return 1;
742 }
743 }
744
745 std::vector<uint32_t> binary_out;
746 spvtools::fuzz::protobufs::TransformationSequence transformations_applied;
747
748 spv_target_env target_env = kDefaultEnvironment;
749
750 switch (status.action) {
751 case FuzzActions::FORCE_RENDER_RED:
752 if (!spvtools::fuzz::ForceRenderRed(
753 target_env, validator_options, binary_in, initial_facts,
754 spvtools::utils::CLIMessageConsumer, &binary_out)) {
755 return 1;
756 }
757 break;
758 case FuzzActions::FUZZ:
759 if (!Fuzz(target_env, fuzzer_options, validator_options, binary_in,
760 initial_facts, donors_file, repeated_pass_strategy,
761 fuzzing_target, &binary_out, &transformations_applied)) {
762 return 1;
763 }
764 break;
765 case FuzzActions::REPLAY:
766 if (!Replay(target_env, fuzzer_options, validator_options, binary_in,
767 initial_facts, replay_transformations_file, &binary_out,
768 &transformations_applied)) {
769 return 1;
770 }
771 break;
772 case FuzzActions::SHRINK: {
773 if (!CheckExecuteCommand()) {
774 std::cerr << "could not find shell interpreter for executing a command"
775 << std::endl;
776 return 1;
777 }
778 if (!Shrink(target_env, fuzzer_options, validator_options, binary_in,
779 initial_facts, shrink_transformations_file,
780 shrink_temp_file_prefix, interestingness_test, &binary_out,
781 &transformations_applied)) {
782 return 1;
783 }
784 } break;
785 default:
786 assert(false && "Unknown fuzzer action.");
787 break;
788 }
789
790 if (!WriteFile<uint32_t>(out_binary_file.c_str(), "wb", binary_out.data(),
791 binary_out.size())) {
792 spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error writing out binary");
793 return 1;
794 }
795
796 if (status.action != FuzzActions::FORCE_RENDER_RED) {
797 // If not found, dot_pos will be std::string::npos, which can be used in
798 // substr to mean "the end of the string"; there is no need to check the
799 // result.
800 dot_pos = out_binary_file.rfind('.');
801 std::string output_file_prefix = out_binary_file.substr(0, dot_pos);
802 std::ofstream transformations_file;
803 transformations_file.open(output_file_prefix + ".transformations",
804 std::ios::out | std::ios::binary);
805 bool success =
806 transformations_applied.SerializeToOstream(&transformations_file);
807 transformations_file.close();
808 if (!success) {
809 spvtools::Error(FuzzDiagnostic, nullptr, {},
810 "Error writing out transformations binary");
811 return 1;
812 }
813
814 std::string json_string;
815 auto json_options = google::protobuf::util::JsonOptions();
816 json_options.add_whitespace = true;
817 auto json_generation_status = google::protobuf::util::MessageToJsonString(
818 transformations_applied, &json_string, json_options);
819 if (!json_generation_status.ok()) {
820 spvtools::Error(FuzzDiagnostic, nullptr, {},
821 "Error writing out transformations in JSON format");
822 return 1;
823 }
824
825 std::ofstream transformations_json_file(output_file_prefix +
826 ".transformations_json");
827 transformations_json_file << json_string;
828 transformations_json_file.close();
829 }
830
831 return 0;
832 }
833