• 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 <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