# Finding a Vulkan driver bug using `spirv-fuzz` `spirv-fuzz` is a tool that automatically finds bugs in Vulkan drivers, specifically the SPIR-V shader compiler component of the driver. The result is an input that, when run on the Vulkan driver, causes the driver to crash or, more generally, "do the wrong thing"; e.g. render an incorrect image. > If you just want to find bugs in Vulkan drivers as quickly as possible then your best bet is probably to run [gfauto](https://github.com/google/graphicsfuzz/tree/master/gfauto#gfauto), which can use `spirv-fuzz` (as well as other tools) to do continuous fuzzing of desktop and Android Vulkan drivers. However, this walkthrough explores using `spirv-fuzz` and `spirv-reduce` as standalone command line tools. As well as being a supported use-case, this also shows what is going on behind-the-scenes when you use `gfauto`. In this walkthrough, we will write a simple application (in [AmberScript](https://github.com/google/amber/blob/master/docs/amber_script.md)) that uses the Vulkan API to render a red square. We will then run `spirv-fuzz` to find a bug in a Vulkan driver and then use the "shrink" mode of `spirv-fuzz`, plus `spirv-reduce`, to reduce the bug-inducing input. We will end up with a much simpler input that still triggers the bug and is suitable for reporting to the driver developers. ## Getting the tools [![Binder](https://mybinder.org/badge_logo.svg) This walkthrough can be run interactively in your browser by clicking here](https://mybinder.org/v2/gh/google/graphicsfuzz/ac20933c3983a84fea2122edf5a518a452cbba89?filepath=docs%2Ffinding-a-vulkan-driver-bug-using-spirv-fuzz.md). You can use Shift+Enter to execute the Bash snippets and see the output. Alternatively, you can copy and paste the Bash snippets into your terminal on a Linux x86 64-bit machine. You can also just read it, but that might be less fun! The following snippet downloads and extracts prebuilt versions of the following tools: * [Amber](https://github.com/google/amber): a tool that executes AmberScript files. An AmberScript file (written in [AmberScript](https://github.com/google/amber/blob/master/docs/amber_script.md)) allows you to concisely list graphics commands that will execute on graphics APIs, like Vulkan. We will use AmberScript to write a simple "Vulkan program" that draws a square, without having to write ~1000 lines of C++. * [SwiftShader](https://github.com/google/swiftshader): a Vulkan driver that uses your CPU (no GPU required!). * [glslangValidator](https://github.com/KhronosGroup/glslang): a tool that compiles shaders written in GLSL (the OpenGL Shading Language). Shaders are essentially programs that are compiled and run on the GPU (or CPU in the case of SwiftShader) to render hardware-accelerated graphics. We will use `glslangValidator` to compile a GLSL shader into SPIR-V (the binary intermediate representation used by Vulkan) suitable for use in our Vulkan program. * [SPIRV-Tools](https://github.com/KhronosGroup/SPIRV-Tools): a suite of tools for SPIR-V files. We will use: * `spirv-fuzz`: the fuzzer itself. * `spirv-reduce`: a tool that simplifies SPIR-V by repeatedly removing SPIR-V instructions. * `spirv-val`: a validator that finds issues with your SPIR-V. * `spirv-dis`: a SPIR-V disassembler that converts SPIR-V (which is a binary format) to human-readable assembly text. * `spirv-as`: a SPIR-V assembler that converts SPIR-V assembly text back to SPIR-V. ```bash curl -fsSL -o amber.zip https://github.com/google/gfbuild-amber/releases/download/github%2Fgoogle%2Fgfbuild-amber%2Fd8acae641ea278ae6a1571797f7bf08747265f15/gfbuild-amber-d8acae641ea278ae6a1571797f7bf08747265f15-Linux_x64_Release.zip unzip -d amber amber.zip curl -fsSL -o swiftshader.zip https://github.com/google/gfbuild-swiftshader/releases/download/github%2Fgoogle%2Fgfbuild-swiftshader%2Ff9f999f5a2eb6dd586a1da03e6b400d044ae6022/gfbuild-swiftshader-f9f999f5a2eb6dd586a1da03e6b400d044ae6022-Linux_x64_Release.zip unzip -d swiftshader swiftshader.zip curl -fsSL -o glslang.zip https://github.com/google/gfbuild-glslang/releases/download/github%2Fgoogle%2Fgfbuild-glslang%2Fae59435606fc5bc453cf4e32320e6579ff7ea22e/gfbuild-glslang-ae59435606fc5bc453cf4e32320e6579ff7ea22e-Linux_x64_Release.zip unzip -d glslang glslang.zip curl -fsSL -o SPIRV-Tools.zip https://github.com/google/gfbuild-SPIRV-Tools/releases/download/github%2Fgoogle%2Fgfbuild-SPIRV-Tools%2F6c218ec60b5f6b525f1badb60c820cae20bd4df3/gfbuild-SPIRV-Tools-6c218ec60b5f6b525f1badb60c820cae20bd4df3-Linux_x64_Release.zip unzip -d SPIRV-Tools SPIRV-Tools.zip ``` The following snippet sets up your environment so we can execute the tools. ```bash export PATH="$(pwd)/glslang/bin:$(pwd)/SPIRV-Tools/bin:$(pwd)/amber/bin:${PATH}" # Note for the curious: this prebuilt Amber comes with a prebuilt Vulkan loader for convenience, which we add to LD_LIBRARY_PATH. export LD_LIBRARY_PATH="$(pwd)/amber/lib:${LD_LIBRARY_PATH}" export VK_ICD_FILENAMES="$(pwd)/swiftshader/lib/vk_swiftshader_icd.json" ``` You should now be able to run Amber. ```bash amber -d -V ``` It should output something like the following, indicating that the SwiftShader Vulkan device was found. ``` Amber : d8acae6 SPIRV-Tools : 97f1d485 SPIRV-Headers: dc77030 GLSLang : 07a55839 Shaderc : 821d564 Physical device properties: apiVersion: 1.1.0 driverVersion: 20971520 vendorID: 6880 deviceID: 49374 deviceType: cpu deviceName: SwiftShader Device (LLVM 7.0.1) driverName: SwiftShader driver driverInfo: End of physical device properties. Summary: 0 pass, 0 fail ``` ## Creating a SPIR-V fragment shader We start by writing a simple Vulkan application (using AmberScript) that will render a red square. To do that, we first need to write a fragment shader. We will write the shader in GLSL (a high-level language) and compile it to SPIR-V (a low-level, binary, intermediate representation for Vulkan). The fragment shader is essentially a program that will run on the graphics device (normally your GPU, but in this case SwiftShader) for every pixel that is rendered, and the output from the program is the pixel color. Here is our fragment shader written in GLSL: ```glsl #version 310 es precision highp float; layout(location = 0) out vec4 color; void main() { color = vec4(1.0, 0.0, 0.0, 1.0); } ``` You do not need to understand all the details, but in brief: the `main` function will be executed for every pixel that is rendered and the output is a vector of 4 elements `(1.0, 0.0, 0.0, 1.0)`, where each element represents the red, green, blue, and alpha color components respectively (each in the range `0.0` to `1.0`). In this case, the output from `main` is always the color red. The following snippet writes our shader to `shader.frag`. ```bash cat >shader.frag <simple.amber <run_shader.sh <simple.amber <>simple.amber cat >>simple.amber <shader.frag < Exercise: try changing the shader to render a gradient from red to green. You can get the x- and y- pixel coordinate using `gl_FragCoord.x` and `gl_FragCoord.y`. The coordinates will be between `0.0` and `255.0`. You can modify and re-execute the snippet above. ## Running `spirv-fuzz` on our shader The `spirv-fuzz` tool takes as input a SPIR-V file, applies many *semantics preserving transformations*, and outputs a transformed SPIR-V file that should be more complex than (but otherwise have the same semantics as) the original input SPIR-V file. > An example of one (very) simple transformation that could be applied is: add the following code (expressed as GLSL): > * `if(false) { return; }`. Thus, using the transformed shader in place of the original should not change the rendered image. If the image *does* change, or the Vulkan driver crashes, then we have found a bug in the Vulkan driver. > The fact that the transformed shader has the same semantics as the original is the key novelty of the *metamorphic testing* fuzzing approach. You can read more about the general approach on the [GraphicsFuzz](https://github.com/google/graphicsfuzz) page, which includes links to our [papers](https://github.com/google/graphicsfuzz#academic-publications) and a ["how it works" page](https://github.com/google/graphicsfuzz/blob/master/docs/glsl-fuzz-intro.md). In short, because the transformed shader is both valid and semantically equivalent, we know when the driver does the wrong thing because the rendered image will change or the driver will crash. In contrast, if we used a traditional fuzzer to generate an arbitrary input (array of bytes), we would not know if the driver was rendering the wrong image because we don't know what the expected image is. We also don't know what a crash means because the arbitrary SPIR-V file might be invalid, and Vulkan drivers are allowed to crash when given invalid SPIR-V. The `spirv-fuzz` tool also takes a list of *donor* SPIR-V files; it can use chunks of code from the donors to make the transformed output file more interesting. You can also provide a _facts file_ alongside the input SPIR-V file to inform `spirv-fuzz` about certain facts that will hold at runtime, which can also make the output SPIR-V file more interesting. However, we will not use any donors or facts in this walkthough. ```bash # Create an empty donors list. touch donors.txt # Run spirv-fuzz to generate a transformed shader "fuzzed.spv". spirv-fuzz shader.spv -o fuzzed.spv --donors=donors.txt ``` There should be some output files: ```bash ls fuzzed.* ``` The output files are: * `fuzzed.spv`: the transformed SPIR-V file. * `fuzzed.transformations`: the list of semantics preserving transformations that were applied to `shader.spv` to get to `fuzzed.spv`. The file is in a binary (protobuf) format. * `fuzzed.transformations_json`: the same as `fuzzed.transformations` but in a human-readable JSON format. You can see the transformations that were applied: ```bash cat fuzzed.transformations_json ``` You do not need to understand the encoding of the transformations, but know that the list of transformations was applied in order to get to `shader.spv`. You can run the transformed shader using our "run shader" script: ```bash # Run the shader. ./run_shader.sh fuzzed.spv echo bash_kernel: saved image data to: output.png ``` You will most likely see the same output as before, and `output.png` will continue to be a green square (or whatever you changed `shader.spv` to render). You can keep trying to see if you get a crash or a different image by repeatedly running the following snippet: ```bash spirv-fuzz shader.spv -o fuzzed.spv --donors=donors.txt ./run_shader.sh fuzzed.spv echo bash_kernel: saved image data to: output.png ``` The outputs from `spirv-fuzz` will be different every time. ## Finding a Vulkan driver bug using `spirv-fuzz` Unfortunately for this walkthrough (but fortunately for the Vulkan ecosystem), finding a bug in our Vulkan driver (SwiftShader) is non-trivial. Furthermore, because our input shader is very simple and we are not using donors nor shader facts, we are very unlikely to be able to find a bug in *any* Vulkan driver. Instead, here is a slightly more interesting shader we made earlier. Execute the following snippet to create the shader `almost_interesting.spv`. ```bash spirv-as --preserve-numeric-ids --target-env spv1.0 -o almost_interesting.spv < Spoiler: the bug really *is* in SwiftShader but our point is that, in general, we try to be careful. We do not assume that our tools are bug-free. ## Shrinking In this walkthrough, `fuzzed.spv` is a fairly small shader. In practice, our fuzzed shaders can be very large due to the large number of transformations applied, nearly all of which add code. Trying to investigate/debug the fuzzed shaders would be difficult. More importantly, in most cases, only a tiny subset of the transformations are needed to get the bug-inducing shader. Thus, we should "shrink" the list of transformations applied to get a much simpler shader that still induces the bug. We should do this before doing further investigation or reporting the bug to the driver developers. > We (and others) normally use the term *reducing* or *reduction* instead of shrinking. However, with `spirv-fuzz`, we use the term shrinking to refer to reducing the list of transformations and *reduction* to refer to reducing SPIR-V by just removing chunks of code (which we will cover in the next section). > Note that there are 44 transformations in `fuzzed.transformations_json`; we will see how many are left after shrinking. To use the shrink mode of `spirv-fuzz`, we need a script that can be used to execute a shader and that gives an exit status of 0 if and only if the shader is still "interesting"; i.e. if and only if the shader causes a segfault. Unfortunately, our shader runner script is close to the opposite of this: ```bash ./run_shader.sh fuzzed.spv echo $? ``` The output is `139`, which is the exit status for a segfault. The following snippet creates a modified shader runner, `run_shader_expect_segfault.sh`, that exits with a status of `0` if and only if the shader causes a segfault: ```bash cat >run_shader_expect_segfault.sh <simple.amber <>simple.amber cat >>simple.amber <