xref: /aosp_15_r20/external/angle/third_party/spirv-tools/src/tools/fuzz/fuzz.cpp (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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(donor_filename.c_str(), &donor_binary)) {
588             return nullptr;
589           }
590           return spvtools::BuildModule(target_env, message_consumer,
591                                        donor_binary.data(),
592                                        donor_binary.size());
593         });
594   }
595 
596   std::unique_ptr<spvtools::opt::IRContext> ir_context;
597   if (!spvtools::fuzz::fuzzerutil::BuildIRContext(target_env, message_consumer,
598                                                   binary_in, validator_options,
599                                                   &ir_context)) {
600     spvtools::Error(FuzzDiagnostic, nullptr, {}, "Initial binary is invalid");
601     return false;
602   }
603 
604   assert((fuzzing_target == FuzzingTarget::kWgsl ||
605           fuzzing_target == FuzzingTarget::kSpirv) &&
606          "Not all fuzzing targets are handled");
607   auto fuzzer_context = spvtools::MakeUnique<spvtools::fuzz::FuzzerContext>(
608       spvtools::MakeUnique<spvtools::fuzz::PseudoRandomGenerator>(
609           fuzzer_options->has_random_seed
610               ? fuzzer_options->random_seed
611               : static_cast<uint32_t>(std::random_device()())),
612       spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()),
613       fuzzing_target == FuzzingTarget::kWgsl);
614 
615   auto transformation_context =
616       spvtools::MakeUnique<spvtools::fuzz::TransformationContext>(
617           spvtools::MakeUnique<spvtools::fuzz::FactManager>(ir_context.get()),
618           validator_options);
619   transformation_context->GetFactManager()->AddInitialFacts(message_consumer,
620                                                             initial_facts);
621 
622   spvtools::fuzz::Fuzzer fuzzer(
623       std::move(ir_context), std::move(transformation_context),
624       std::move(fuzzer_context), message_consumer, donor_suppliers,
625       fuzzer_options->all_passes_enabled, repeated_pass_strategy,
626       fuzzer_options->fuzzer_pass_validation_enabled, validator_options, false);
627   auto fuzz_result = fuzzer.Run(0);
628   if (fuzz_result.status ==
629       spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule) {
630     spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error running fuzzer");
631     return false;
632   }
633 
634   fuzzer.GetIRContext()->module()->ToBinary(binary_out, true);
635   *transformations_applied = fuzzer.GetTransformationSequence();
636   return true;
637 }
638 
639 }  // namespace
640 
641 // Dumps |binary| to file |filename|. Useful for interactive debugging.
DumpShader(const std::vector<uint32_t> & binary,const char * filename)642 void DumpShader(const std::vector<uint32_t>& binary, const char* filename) {
643   auto write_file_succeeded =
644       WriteFile(filename, "wb", &binary[0], binary.size());
645   if (!write_file_succeeded) {
646     std::cerr << "Failed to dump shader" << std::endl;
647   }
648 }
649 
650 // Dumps the SPIRV-V module in |context| to file |filename|. Useful for
651 // interactive debugging.
DumpShader(spvtools::opt::IRContext * context,const char * filename)652 void DumpShader(spvtools::opt::IRContext* context, const char* filename) {
653   std::vector<uint32_t> binary;
654   context->module()->ToBinary(&binary, false);
655   DumpShader(binary, filename);
656 }
657 
658 // Dumps |transformations| to file |filename| in binary format. Useful for
659 // interactive debugging.
DumpTransformationsBinary(const spvtools::fuzz::protobufs::TransformationSequence & transformations,const char * filename)660 void DumpTransformationsBinary(
661     const spvtools::fuzz::protobufs::TransformationSequence& transformations,
662     const char* filename) {
663   std::ofstream transformations_file;
664   transformations_file.open(filename, std::ios::out | std::ios::binary);
665   transformations.SerializeToOstream(&transformations_file);
666   transformations_file.close();
667 }
668 
669 // Dumps |transformations| to file |filename| in JSON format. Useful for
670 // interactive debugging.
DumpTransformationsJson(const spvtools::fuzz::protobufs::TransformationSequence & transformations,const char * filename)671 void DumpTransformationsJson(
672     const spvtools::fuzz::protobufs::TransformationSequence& transformations,
673     const char* filename) {
674   std::string json_string;
675   auto json_options = google::protobuf::util::JsonPrintOptions();
676   json_options.add_whitespace = true;
677   auto json_generation_status = google::protobuf::util::MessageToJsonString(
678       transformations, &json_string, json_options);
679   if (json_generation_status.ok()) {
680     std::ofstream transformations_json_file(filename);
681     transformations_json_file << json_string;
682     transformations_json_file.close();
683   }
684 }
685 
686 const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
687 
main(int argc,const char ** argv)688 int main(int argc, const char** argv) {
689   std::string in_binary_file;
690   std::string out_binary_file;
691   std::string donors_file;
692   std::string replay_transformations_file;
693   std::vector<std::string> interestingness_test;
694   std::string shrink_transformations_file;
695   std::string shrink_temp_file_prefix = "temp_";
696   spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy;
697   auto fuzzing_target = FuzzingTarget::kSpirv;
698 
699   spvtools::FuzzerOptions fuzzer_options;
700   spvtools::ValidatorOptions validator_options;
701 
702   FuzzStatus status =
703       ParseFlags(argc, argv, &in_binary_file, &out_binary_file, &donors_file,
704                  &replay_transformations_file, &interestingness_test,
705                  &shrink_transformations_file, &shrink_temp_file_prefix,
706                  &repeated_pass_strategy, &fuzzing_target, &fuzzer_options,
707                  &validator_options);
708 
709   if (status.action == FuzzActions::STOP) {
710     return status.code;
711   }
712 
713   std::vector<uint32_t> binary_in;
714   if (!ReadBinaryFile(in_binary_file.c_str(), &binary_in)) {
715     return 1;
716   }
717 
718   spvtools::fuzz::protobufs::FactSequence initial_facts;
719 
720   // If not found, dot_pos will be std::string::npos, which can be used in
721   // substr to mean "the end of the string"; there is no need to check the
722   // result.
723   size_t dot_pos = in_binary_file.rfind('.');
724   std::string in_facts_file = in_binary_file.substr(0, dot_pos) + ".facts";
725   std::ifstream facts_input(in_facts_file);
726   if (facts_input) {
727     std::string facts_json_string((std::istreambuf_iterator<char>(facts_input)),
728                                   std::istreambuf_iterator<char>());
729     facts_input.close();
730     if (!google::protobuf::util::JsonStringToMessage(facts_json_string,
731                                                      &initial_facts)
732              .ok()) {
733       spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error reading facts data");
734       return 1;
735     }
736   }
737 
738   std::vector<uint32_t> binary_out;
739   spvtools::fuzz::protobufs::TransformationSequence transformations_applied;
740 
741   spv_target_env target_env = kDefaultEnvironment;
742 
743   switch (status.action) {
744     case FuzzActions::FORCE_RENDER_RED:
745       if (!spvtools::fuzz::ForceRenderRed(
746               target_env, validator_options, binary_in, initial_facts,
747               spvtools::utils::CLIMessageConsumer, &binary_out)) {
748         return 1;
749       }
750       break;
751     case FuzzActions::FUZZ:
752       if (!Fuzz(target_env, fuzzer_options, validator_options, binary_in,
753                 initial_facts, donors_file, repeated_pass_strategy,
754                 fuzzing_target, &binary_out, &transformations_applied)) {
755         return 1;
756       }
757       break;
758     case FuzzActions::REPLAY:
759       if (!Replay(target_env, fuzzer_options, validator_options, binary_in,
760                   initial_facts, replay_transformations_file, &binary_out,
761                   &transformations_applied)) {
762         return 1;
763       }
764       break;
765     case FuzzActions::SHRINK: {
766       if (!Shrink(target_env, fuzzer_options, validator_options, binary_in,
767                   initial_facts, shrink_transformations_file,
768                   shrink_temp_file_prefix, interestingness_test, &binary_out,
769                   &transformations_applied)) {
770         return 1;
771       }
772     } break;
773     default:
774       assert(false && "Unknown fuzzer action.");
775       break;
776   }
777 
778   if (!WriteFile<uint32_t>(out_binary_file.c_str(), "wb", binary_out.data(),
779                            binary_out.size())) {
780     spvtools::Error(FuzzDiagnostic, nullptr, {}, "Error writing out binary");
781     return 1;
782   }
783 
784   if (status.action != FuzzActions::FORCE_RENDER_RED) {
785     // If not found, dot_pos will be std::string::npos, which can be used in
786     // substr to mean "the end of the string"; there is no need to check the
787     // result.
788     dot_pos = out_binary_file.rfind('.');
789     std::string output_file_prefix = out_binary_file.substr(0, dot_pos);
790     std::ofstream transformations_file;
791     transformations_file.open(output_file_prefix + ".transformations",
792                               std::ios::out | std::ios::binary);
793     bool success =
794         transformations_applied.SerializeToOstream(&transformations_file);
795     transformations_file.close();
796     if (!success) {
797       spvtools::Error(FuzzDiagnostic, nullptr, {},
798                       "Error writing out transformations binary");
799       return 1;
800     }
801 
802     std::string json_string;
803     auto json_options = google::protobuf::util::JsonPrintOptions();
804     json_options.add_whitespace = true;
805     auto json_generation_status = google::protobuf::util::MessageToJsonString(
806         transformations_applied, &json_string, json_options);
807     if (!json_generation_status.ok()) {
808       spvtools::Error(FuzzDiagnostic, nullptr, {},
809                       "Error writing out transformations in JSON format");
810       return 1;
811     }
812 
813     std::ofstream transformations_json_file(output_file_prefix +
814                                             ".transformations_json");
815     transformations_json_file << json_string;
816     transformations_json_file.close();
817   }
818 
819   return 0;
820 }
821