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