xref: /third_party/spirv-tools/tools/fuzz/fuzz.cpp (revision fd4e5da5)
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
40namespace {
41
42enum 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.
46bool 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.
57enum 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
67struct FuzzStatus {
68  FuzzActions action;
69  int code;
70};
71
72void 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
77USAGE: %s [options] <input.spv> -o <output.spv> \
78  --donors=<donors.txt>
79USAGE: %s [options] <input.spv> -o <output.spv> \
80  --shrink=<input.transformations> -- <interestingness_test> [args...]
81
82The SPIR-V binary is read from <input.spv>.  If <input.facts> is also present,
83facts about the SPIR-V binary are read from this file.
84
85The transformed SPIR-V binary is written to <output.spv>.  Human-readable and
86binary representations of the transformations that were applied are written to
87<output.transformations_json> and <output.transformations>, respectively.
88
89When passing --shrink=<input.transformations> an <interestingness_test>
90must also be provided; this is the path to a script that returns 0 if and only
91if a given SPIR-V binary is interesting.  The SPIR-V binary will be passed to
92the script as an argument after any other provided arguments [args...].  The
93"--" characters are optional but denote that all arguments that follow are
94positional arguments and thus will be forwarded to the interestingness script,
95and not parsed by %s.
96
97NOTE: The fuzzer is a work in progress.
98
99Options (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
180Supported 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.
194void 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
202FuzzStatus 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
442bool 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
461bool 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
505bool 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
562bool 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.
643void 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.
653void 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.
661void 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.
672void 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
687const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
688
689int 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