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