• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2023 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.durationStringToSeconds;
18 import static com.code_intelligence.jazzer.junit.Utils.generatedCorpusPath;
19 import static com.code_intelligence.jazzer.junit.Utils.inputsDirectoryResourcePath;
20 import static com.code_intelligence.jazzer.junit.Utils.inputsDirectorySourcePath;
21 
22 import com.code_intelligence.jazzer.agent.AgentInstaller;
23 import com.code_intelligence.jazzer.driver.FuzzTargetHolder;
24 import com.code_intelligence.jazzer.driver.FuzzTargetRunner;
25 import com.code_intelligence.jazzer.driver.Opt;
26 import com.code_intelligence.jazzer.driver.junit.ExitCodeException;
27 import java.io.File;
28 import java.io.IOException;
29 import java.lang.reflect.Executable;
30 import java.lang.reflect.Method;
31 import java.net.URISyntaxException;
32 import java.net.URL;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.Paths;
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Optional;
42 import java.util.concurrent.atomic.AtomicBoolean;
43 import java.util.concurrent.atomic.AtomicReference;
44 import java.util.stream.Collectors;
45 import java.util.stream.Stream;
46 import org.junit.jupiter.api.extension.ExtensionContext;
47 import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
48 import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
49 import org.junit.jupiter.params.provider.ArgumentsSource;
50 import org.junit.platform.commons.support.AnnotationSupport;
51 
52 class FuzzTestExecutor {
53   private static final AtomicBoolean hasBeenPrepared = new AtomicBoolean();
54   private static final AtomicBoolean agentInstalled = new AtomicBoolean(false);
55 
56   private final List<String> libFuzzerArgs;
57   private final Path javaSeedsDir;
58   private final boolean isRunFromCommandLine;
59 
FuzzTestExecutor( List<String> libFuzzerArgs, Path javaSeedsDir, boolean isRunFromCommandLine)60   private FuzzTestExecutor(
61       List<String> libFuzzerArgs, Path javaSeedsDir, boolean isRunFromCommandLine) {
62     this.libFuzzerArgs = libFuzzerArgs;
63     this.javaSeedsDir = javaSeedsDir;
64     this.isRunFromCommandLine = isRunFromCommandLine;
65   }
66 
prepare(ExtensionContext context, String maxDuration)67   public static FuzzTestExecutor prepare(ExtensionContext context, String maxDuration)
68       throws IOException {
69     if (!hasBeenPrepared.compareAndSet(false, true)) {
70       throw new IllegalStateException(
71           "FuzzTestExecutor#prepare can only be called once per test run");
72     }
73 
74     Class<?> fuzzTestClass = context.getRequiredTestClass();
75     Method fuzzTestMethod = context.getRequiredTestMethod();
76 
77     List<ArgumentsSource> allSources = AnnotationSupport.findRepeatableAnnotations(
78         context.getRequiredTestMethod(), ArgumentsSource.class);
79     // Non-empty as it always contains FuzzingArgumentsProvider.
80     ArgumentsSource lastSource = allSources.get(allSources.size() - 1);
81     // Ensure that our ArgumentsProviders run last so that we can record all the seeds generated by
82     // user-provided ones.
83     if (lastSource.value().getPackage() != FuzzTestExecutor.class.getPackage()) {
84       throw new IllegalArgumentException("@FuzzTest must be the last annotation on a fuzz test,"
85           + " but it came after the (meta-)annotation " + lastSource);
86     }
87 
88     Path baseDir =
89         Paths.get(context.getConfigurationParameter("jazzer.internal.basedir").orElse(""))
90             .toAbsolutePath();
91 
92     List<String> originalLibFuzzerArgs = getLibFuzzerArgs(context);
93     String argv0 = originalLibFuzzerArgs.isEmpty() ? "fake_argv0" : originalLibFuzzerArgs.remove(0);
94 
95     ArrayList<String> libFuzzerArgs = new ArrayList<>();
96     libFuzzerArgs.add(argv0);
97 
98     // Add passed in corpus directories (and files) at the beginning of the arguments list.
99     // libFuzzer uses the first directory to store discovered inputs, whereas all others are
100     // only used to provide additional seeds and aren't written into.
101     List<String> corpusDirs = originalLibFuzzerArgs.stream()
102                                   .filter(arg -> !arg.startsWith("-"))
103                                   .collect(Collectors.toList());
104     originalLibFuzzerArgs.removeAll(corpusDirs);
105     libFuzzerArgs.addAll(corpusDirs);
106 
107     // Use the specified corpus dir, if given, otherwise store the generated corpus in a per-class
108     // directory under the project root, just like cifuzz:
109     // https://github.com/CodeIntelligenceTesting/cifuzz/blob/bf410dcfbafbae2a73cf6c5fbed031cdfe234f2f/internal/cmd/run/run.go#L381
110     // The path is specified relative to the current working directory, which with JUnit is the
111     // project directory.
112     Path generatedCorpusDir = baseDir.resolve(generatedCorpusPath(fuzzTestClass, fuzzTestMethod));
113     Files.createDirectories(generatedCorpusDir);
114     libFuzzerArgs.add(generatedCorpusDir.toAbsolutePath().toString());
115 
116     // We can only emit findings into the source tree version of the inputs directory, not e.g. the
117     // copy under Maven's target directory. If it doesn't exist, collect the inputs in the current
118     // working directory, which is usually the project's source root.
119     Optional<Path> findingsDirectory =
120         inputsDirectorySourcePath(fuzzTestClass, fuzzTestMethod, baseDir);
121     if (!findingsDirectory.isPresent()) {
122       context.publishReportEntry(String.format(
123           "Collecting crashing inputs in the project root directory.\nIf you want to keep them "
124               + "organized by fuzz test and automatically run them as regression tests with "
125               + "JUnit Jupiter, create a test resource directory called '%s' in package '%s' "
126               + "and move the files there.",
127           inputsDirectoryResourcePath(fuzzTestClass, fuzzTestMethod),
128           fuzzTestClass.getPackage().getName()));
129     }
130 
131     // We prefer the inputs directory on the classpath, if it exists, as that is more reliable than
132     // heuristically looking into the source tree based on the current working directory.
133     Optional<Path> inputsDirectory;
134     URL inputsDirectoryUrl =
135         fuzzTestClass.getResource(inputsDirectoryResourcePath(fuzzTestClass, fuzzTestMethod));
136     if (inputsDirectoryUrl != null && "file".equals(inputsDirectoryUrl.getProtocol())) {
137       // The inputs directory is a regular directory on disk (i.e., the test is not run from a
138       // JAR).
139       try {
140         // Using inputsDirectoryUrl.getFile() fails on Windows.
141         inputsDirectory = Optional.of(Paths.get(inputsDirectoryUrl.toURI()));
142       } catch (URISyntaxException e) {
143         throw new RuntimeException(e);
144       }
145     } else {
146       if (inputsDirectoryUrl != null && !findingsDirectory.isPresent()) {
147         context.publishReportEntry(
148             "When running Jazzer fuzz tests from a JAR rather than class files, the inputs "
149             + "directory isn't used unless it is located under src/test/resources/...");
150       }
151       inputsDirectory = findingsDirectory;
152     }
153 
154     // From the second positional argument on, files and directories are used as seeds but not
155     // modified.
156     inputsDirectory.ifPresent(dir -> libFuzzerArgs.add(dir.toAbsolutePath().toString()));
157     Path javaSeedsDir = Files.createTempDirectory("jazzer-java-seeds");
158     libFuzzerArgs.add(javaSeedsDir.toAbsolutePath().toString());
159     libFuzzerArgs.add(String.format("-artifact_prefix=%s%c",
160         findingsDirectory.orElse(baseDir).toAbsolutePath(), File.separatorChar));
161 
162     libFuzzerArgs.add("-max_total_time=" + durationStringToSeconds(maxDuration));
163     // Disable libFuzzer's out of memory detection: It is only useful for native library fuzzing,
164     // which we don't support without our native driver, and leads to false positives where it picks
165     // up IntelliJ's memory usage.
166     libFuzzerArgs.add("-rss_limit_mb=0");
167     if (Utils.permissivelyParseBoolean(
168             context.getConfigurationParameter("jazzer.valueprofile").orElse("false"))) {
169       libFuzzerArgs.add("-use_value_profile=1");
170     }
171 
172     // Prefer original libFuzzerArgs set via command line by appending them last.
173     libFuzzerArgs.addAll(originalLibFuzzerArgs);
174 
175     return new FuzzTestExecutor(libFuzzerArgs, javaSeedsDir, Utils.runFromCommandLine(context));
176   }
177 
178   /**
179    * Returns the list of arguments set on the command line.
180    */
getLibFuzzerArgs(ExtensionContext extensionContext)181   private static List<String> getLibFuzzerArgs(ExtensionContext extensionContext) {
182     List<String> args = new ArrayList<>();
183     for (int i = 0;; i++) {
184       Optional<String> arg = extensionContext.getConfigurationParameter("jazzer.internal.arg." + i);
185       if (!arg.isPresent()) {
186         break;
187       }
188       args.add(arg.get());
189     }
190     return args;
191   }
192 
configureAndInstallAgent(ExtensionContext extensionContext, String maxDuration)193   static void configureAndInstallAgent(ExtensionContext extensionContext, String maxDuration)
194       throws IOException {
195     if (!agentInstalled.compareAndSet(false, true)) {
196       return;
197     }
198     if (Utils.isFuzzing(extensionContext)) {
199       FuzzTestExecutor executor = prepare(extensionContext, maxDuration);
200       extensionContext.getRoot().getStore(Namespace.GLOBAL).put(FuzzTestExecutor.class, executor);
201       AgentConfigurator.forFuzzing(extensionContext);
202     } else {
203       AgentConfigurator.forRegressionTest(extensionContext);
204     }
205     AgentInstaller.install(Opt.hooks);
206   }
207 
fromContext(ExtensionContext extensionContext)208   static FuzzTestExecutor fromContext(ExtensionContext extensionContext) {
209     return extensionContext.getRoot()
210         .getStore(Namespace.GLOBAL)
211         .get(FuzzTestExecutor.class, FuzzTestExecutor.class);
212   }
213 
addSeed(byte[] bytes)214   public void addSeed(byte[] bytes) throws IOException {
215     Path seed = Files.createTempFile(javaSeedsDir, "seed", null);
216     Files.write(seed, bytes);
217   }
218 
219   @SuppressWarnings("OptionalGetWithoutIsPresent")
execute( ReflectiveInvocationContext<Method> invocationContext, SeedSerializer seedSerializer)220   public Optional<Throwable> execute(
221       ReflectiveInvocationContext<Method> invocationContext, SeedSerializer seedSerializer) {
222     if (seedSerializer instanceof AutofuzzSeedSerializer) {
223       FuzzTargetHolder.fuzzTarget = FuzzTargetHolder.autofuzzFuzzTarget(() -> {
224         // Provide an empty throws declaration to prevent autofuzz from
225         // ignoring the defined test exceptions. All exceptions in tests
226         // should cause them to fail.
227         Map<Executable, Class<?>[]> throwsDeclarations = new HashMap<>(1);
228         throwsDeclarations.put(invocationContext.getExecutable(), new Class[0]);
229 
230         com.code_intelligence.jazzer.autofuzz.FuzzTarget.setTarget(
231             new Executable[] {invocationContext.getExecutable()},
232             invocationContext.getTarget().get(), invocationContext.getExecutable().toString(),
233             Collections.emptySet(), throwsDeclarations);
234         return null;
235       });
236     } else {
237       FuzzTargetHolder.fuzzTarget =
238           new FuzzTargetHolder.FuzzTarget(invocationContext.getExecutable(),
239               () -> invocationContext.getTarget().get(), Optional.empty());
240     }
241 
242     // Only register a finding handler in case the fuzz test is executed by JUnit.
243     // It short-circuits the handling in FuzzTargetRunner and prevents settings
244     // like --keep_going.
245     AtomicReference<Throwable> atomicFinding = new AtomicReference<>();
246     if (!isRunFromCommandLine) {
247       FuzzTargetRunner.registerFindingHandler(t -> {
248         atomicFinding.set(t);
249         return false;
250       });
251     }
252 
253     int exitCode = FuzzTargetRunner.startLibFuzzer(libFuzzerArgs);
254     deleteJavaSeedsDir();
255     Throwable finding = atomicFinding.get();
256     if (finding != null) {
257       return Optional.of(finding);
258     } else if (exitCode != 0) {
259       return Optional.of(
260           new ExitCodeException("Jazzer exited with exit code " + exitCode, exitCode));
261     } else {
262       return Optional.empty();
263     }
264   }
265 
deleteJavaSeedsDir()266   private void deleteJavaSeedsDir() {
267     // The directory only consists of files, which we need to delete before deleting the directory
268     // itself.
269     try (Stream<Path> entries = Files.list(javaSeedsDir)) {
270       entries.forEach(FuzzTestExecutor::deleteIgnoringErrors);
271     } catch (IOException ignored) {
272     }
273     deleteIgnoringErrors(javaSeedsDir);
274   }
275 
deleteIgnoringErrors(Path path)276   private static void deleteIgnoringErrors(Path path) {
277     try {
278       Files.deleteIfExists(path);
279     } catch (IOException ignored) {
280     }
281   }
282 }
283