1 // Copyright 2022 Code Intelligence GmbH 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 package com.code_intelligence.jazzer.junit; 16 17 import static com.code_intelligence.jazzer.junit.Utils.isFuzzing; 18 import static com.code_intelligence.jazzer.junit.Utils.runFromCommandLine; 19 import static org.junit.jupiter.api.Named.named; 20 import static org.junit.jupiter.params.provider.Arguments.arguments; 21 22 import java.io.IOException; 23 import java.lang.reflect.Method; 24 import java.net.URI; 25 import java.net.URISyntaxException; 26 import java.net.URL; 27 import java.nio.file.FileSystem; 28 import java.nio.file.FileSystems; 29 import java.nio.file.FileVisitOption; 30 import java.nio.file.Files; 31 import java.nio.file.NoSuchFileException; 32 import java.nio.file.Path; 33 import java.nio.file.Paths; 34 import java.util.AbstractMap.SimpleImmutableEntry; 35 import java.util.HashMap; 36 import java.util.Map; 37 import java.util.function.BiPredicate; 38 import java.util.stream.Stream; 39 import org.junit.jupiter.api.extension.ExtensionContext; 40 import org.junit.jupiter.params.provider.Arguments; 41 import org.junit.jupiter.params.provider.ArgumentsProvider; 42 43 class SeedArgumentsProvider implements ArgumentsProvider { 44 @Override provideArguments(ExtensionContext extensionContext)45 public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) 46 throws IOException { 47 if (runFromCommandLine(extensionContext)) { 48 // libFuzzer always runs on the file-based seeds first anyway and the additional visual 49 // indication provided by test invocations for seeds isn't effective on the command line, so 50 // we skip these invocations. 51 return Stream.empty(); 52 } 53 54 Class<?> testClass = extensionContext.getRequiredTestClass(); 55 Method testMethod = extensionContext.getRequiredTestMethod(); 56 57 Stream<Map.Entry<String, byte[]>> rawSeeds = 58 Stream.of(new SimpleImmutableEntry<>("<empty input>", new byte[0])); 59 rawSeeds = Stream.concat(rawSeeds, walkInputs(testClass, testMethod)); 60 61 if (Utils.isCoverageAgentPresent() 62 && Files.isDirectory(Utils.generatedCorpusPath(testClass, testMethod))) { 63 rawSeeds = Stream.concat(rawSeeds, 64 walkInputsInPath(Utils.generatedCorpusPath(testClass, testMethod), Integer.MAX_VALUE)); 65 } 66 67 SeedSerializer serializer = SeedSerializer.of(testMethod); 68 return rawSeeds 69 .map(entry -> { 70 Object[] args = serializer.read(entry.getValue()); 71 args[0] = named(entry.getKey(), args[0]); 72 return arguments(args); 73 }) 74 .onClose(() -> { 75 if (!isFuzzing(extensionContext)) { 76 extensionContext.publishReportEntry( 77 "No fuzzing has been performed, the fuzz test has only been executed on the fixed " 78 + "set of inputs in the seed corpus.\n" 79 + "To start fuzzing, run a test with the environment variable JAZZER_FUZZ set to a " 80 + "non-empty value."); 81 } 82 if (!serializer.allReadsValid()) { 83 extensionContext.publishReportEntry( 84 "Some files in the seed corpus do not match the fuzz target signature.\n" 85 + "This indicates that they were generated with a different signature and may cause " 86 + "issues reproducing previous findings."); 87 } 88 }); 89 } 90 91 /** 92 * Used in regression mode to get test cases for the associated {@code testMethod} 93 * This will return a stream of files consisting of: 94 * <ul> 95 * <li>{@code resources/<classpath>/<testClass name>Inputs/*}</li> 96 * <li>{@code resources/<classpath>/<testClass name>Inputs/<testMethod name>/**}</li> 97 * </ul> 98 * Or the equivalent behavior on resources inside a jar file. 99 * <p> 100 * Note that the first {@code <testClass name>Inputs} path will not recursively search all 101 * directories but only gives files in that directory whereas the {@code <testMethod name>} 102 * directory is searched recursively. This allows for multiple tests to share inputs without 103 * needing to explicitly copy them into each test's directory. 104 * 105 * @param testClass the class of the test being run 106 * @param testMethod the test function being run 107 * @return a stream of findings files to use as inputs for the test function 108 */ walkInputs(Class<?> testClass, Method testMethod)109 private Stream<Map.Entry<String, byte[]>> walkInputs(Class<?> testClass, Method testMethod) 110 throws IOException { 111 URL classInputsDirUrl = testClass.getResource(Utils.inputsDirectoryResourcePath(testClass)); 112 113 if (classInputsDirUrl == null) { 114 return Stream.empty(); 115 } 116 URI classInputsDirUri; 117 try { 118 classInputsDirUri = classInputsDirUrl.toURI(); 119 } catch (URISyntaxException e) { 120 throw new IOException("Failed to open inputs resource directory: " + classInputsDirUrl, e); 121 } 122 if (classInputsDirUri.getScheme().equals("file")) { 123 // The test is executed from class files, which usually happens when run from inside an IDE. 124 Path classInputsPath = Paths.get(classInputsDirUri); 125 126 return Stream.concat( 127 walkClassInputs(classInputsPath), walkTestInputs(classInputsPath, testMethod)); 128 129 } else if (classInputsDirUri.getScheme().equals("jar")) { 130 FileSystem jar = FileSystems.newFileSystem(classInputsDirUri, new HashMap<>()); 131 // inputsDirUrl looks like this: 132 // file:/tmp/testdata/ExampleFuzzTest_deploy.jar!/com/code_intelligence/jazzer/junit/testdata/ExampleFuzzTestInputs 133 String pathInJar = 134 classInputsDirUrl.getFile().substring(classInputsDirUrl.getFile().indexOf('!') + 1); 135 136 Path classPathInJar = jar.getPath(pathInJar); 137 138 return Stream 139 .concat(walkClassInputs(classPathInJar), walkTestInputs(classPathInJar, testMethod)) 140 .onClose(() -> { 141 try { 142 jar.close(); 143 } catch (IOException e) { 144 throw new RuntimeException(e); 145 } 146 }); 147 } else { 148 throw new IOException( 149 "Unsupported protocol for inputs resource directory: " + classInputsDirUrl); 150 } 151 } 152 153 /** 154 * Walks over the inputs for the method being tested, recurses into subdirectories 155 * @param classInputsPath the path of the class being tested, used as the base path where the test 156 * method's directory 157 * should be 158 * @param testMethod the method being tested 159 * @return a stream of all files under {@code <classInputsPath>/<testMethod name>} 160 * @throws IOException can be thrown by the underlying call to {@link Files#find} 161 */ 162 private static Stream<Map.Entry<String, byte[]>> walkTestInputs( 163 Path classInputsPath, Method testMethod) throws IOException { 164 Path testInputsPath = classInputsPath.resolve(testMethod.getName()); 165 try { 166 return walkInputsInPath(testInputsPath, Integer.MAX_VALUE); 167 } catch (NoSuchFileException e) { 168 return Stream.empty(); 169 } 170 } 171 172 /** 173 * Walks over the inputs for the class being tested. Does not recurse into subdirectories 174 * @param path the path to search to files 175 * @return a stream of all files (without directories) within {@code path}. If {@code path} is not 176 * found, an empty 177 * stream is returned. 178 * @throws IOException can be thrown by the underlying call to {@link Files#find} 179 */ 180 private static Stream<Map.Entry<String, byte[]>> walkClassInputs(Path path) throws IOException { 181 try { 182 // using a depth of 1 will get all files within the given path but does not recurse into 183 // subdirectories 184 return walkInputsInPath(path, 1); 185 } catch (NoSuchFileException e) { 186 return Stream.empty(); 187 } 188 } 189 190 /** 191 * Gets a sorted stream of all files (without directories) within under the given {@code path} 192 * @param path the path to walk 193 * @param depth the maximum depth of subdirectories to search from within {@code path}. 1 194 * indicates it should return 195 * only the files directly in {@code path} and not search any of its subdirectories 196 * @return a stream of file name -> file contents as a raw byte array 197 * @throws IOException can be thrown by the call to {@link Files#find(Path, int, BiPredicate, 198 * FileVisitOption...)} 199 */ 200 private static Stream<Map.Entry<String, byte[]>> walkInputsInPath(Path path, int depth) 201 throws IOException { 202 // @ParameterTest automatically closes Streams and AutoCloseable instances. 203 // noinspection resource 204 return Files 205 .find(path, depth, 206 (fileOrDir, basicFileAttributes) 207 -> !basicFileAttributes.isDirectory(), 208 FileVisitOption.FOLLOW_LINKS) 209 // JUnit identifies individual runs of a `@ParameterizedTest` via their invocation number. 210 // In order to get reproducible behavior e.g. when trying to debug a particular input, all 211 // inputs thus have to be provided in deterministic order. 212 .sorted() 213 .map(file 214 -> new SimpleImmutableEntry<>( 215 file.getFileName().toString(), readAllBytesUnchecked(file))); 216 } 217 218 private static byte[] readAllBytesUnchecked(Path path) { 219 try { 220 return Files.readAllBytes(path); 221 } catch (IOException e) { 222 throw new IllegalStateException(e); 223 } 224 } 225 } 226