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