• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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