// Copyright 2022 Code Intelligence GmbH // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.code_intelligence.jazzer.junit; import static com.code_intelligence.jazzer.junit.Utils.isFuzzing; import static com.code_intelligence.jazzer.junit.Utils.runFromCommandLine; import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.params.provider.Arguments.arguments; import java.io.IOException; import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.HashMap; import java.util.Map; import java.util.function.BiPredicate; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; class SeedArgumentsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext extensionContext) throws IOException { if (runFromCommandLine(extensionContext)) { // libFuzzer always runs on the file-based seeds first anyway and the additional visual // indication provided by test invocations for seeds isn't effective on the command line, so // we skip these invocations. return Stream.empty(); } Class testClass = extensionContext.getRequiredTestClass(); Method testMethod = extensionContext.getRequiredTestMethod(); Stream> rawSeeds = Stream.of(new SimpleImmutableEntry<>("", new byte[0])); rawSeeds = Stream.concat(rawSeeds, walkInputs(testClass, testMethod)); if (Utils.isCoverageAgentPresent() && Files.isDirectory(Utils.generatedCorpusPath(testClass, testMethod))) { rawSeeds = Stream.concat(rawSeeds, walkInputsInPath(Utils.generatedCorpusPath(testClass, testMethod), Integer.MAX_VALUE)); } SeedSerializer serializer = SeedSerializer.of(testMethod); return rawSeeds .map(entry -> { Object[] args = serializer.read(entry.getValue()); args[0] = named(entry.getKey(), args[0]); return arguments(args); }) .onClose(() -> { if (!isFuzzing(extensionContext)) { extensionContext.publishReportEntry( "No fuzzing has been performed, the fuzz test has only been executed on the fixed " + "set of inputs in the seed corpus.\n" + "To start fuzzing, run a test with the environment variable JAZZER_FUZZ set to a " + "non-empty value."); } if (!serializer.allReadsValid()) { extensionContext.publishReportEntry( "Some files in the seed corpus do not match the fuzz target signature.\n" + "This indicates that they were generated with a different signature and may cause " + "issues reproducing previous findings."); } }); } /** * Used in regression mode to get test cases for the associated {@code testMethod} * This will return a stream of files consisting of: * * Or the equivalent behavior on resources inside a jar file. *

* Note that the first {@code Inputs} path will not recursively search all * directories but only gives files in that directory whereas the {@code } * directory is searched recursively. This allows for multiple tests to share inputs without * needing to explicitly copy them into each test's directory. * * @param testClass the class of the test being run * @param testMethod the test function being run * @return a stream of findings files to use as inputs for the test function */ private Stream> walkInputs(Class testClass, Method testMethod) throws IOException { URL classInputsDirUrl = testClass.getResource(Utils.inputsDirectoryResourcePath(testClass)); if (classInputsDirUrl == null) { return Stream.empty(); } URI classInputsDirUri; try { classInputsDirUri = classInputsDirUrl.toURI(); } catch (URISyntaxException e) { throw new IOException("Failed to open inputs resource directory: " + classInputsDirUrl, e); } if (classInputsDirUri.getScheme().equals("file")) { // The test is executed from class files, which usually happens when run from inside an IDE. Path classInputsPath = Paths.get(classInputsDirUri); return Stream.concat( walkClassInputs(classInputsPath), walkTestInputs(classInputsPath, testMethod)); } else if (classInputsDirUri.getScheme().equals("jar")) { FileSystem jar = FileSystems.newFileSystem(classInputsDirUri, new HashMap<>()); // inputsDirUrl looks like this: // file:/tmp/testdata/ExampleFuzzTest_deploy.jar!/com/code_intelligence/jazzer/junit/testdata/ExampleFuzzTestInputs String pathInJar = classInputsDirUrl.getFile().substring(classInputsDirUrl.getFile().indexOf('!') + 1); Path classPathInJar = jar.getPath(pathInJar); return Stream .concat(walkClassInputs(classPathInJar), walkTestInputs(classPathInJar, testMethod)) .onClose(() -> { try { jar.close(); } catch (IOException e) { throw new RuntimeException(e); } }); } else { throw new IOException( "Unsupported protocol for inputs resource directory: " + classInputsDirUrl); } } /** * Walks over the inputs for the method being tested, recurses into subdirectories * @param classInputsPath the path of the class being tested, used as the base path where the test * method's directory * should be * @param testMethod the method being tested * @return a stream of all files under {@code /} * @throws IOException can be thrown by the underlying call to {@link Files#find} */ private static Stream> walkTestInputs( Path classInputsPath, Method testMethod) throws IOException { Path testInputsPath = classInputsPath.resolve(testMethod.getName()); try { return walkInputsInPath(testInputsPath, Integer.MAX_VALUE); } catch (NoSuchFileException e) { return Stream.empty(); } } /** * Walks over the inputs for the class being tested. Does not recurse into subdirectories * @param path the path to search to files * @return a stream of all files (without directories) within {@code path}. If {@code path} is not * found, an empty * stream is returned. * @throws IOException can be thrown by the underlying call to {@link Files#find} */ private static Stream> walkClassInputs(Path path) throws IOException { try { // using a depth of 1 will get all files within the given path but does not recurse into // subdirectories return walkInputsInPath(path, 1); } catch (NoSuchFileException e) { return Stream.empty(); } } /** * Gets a sorted stream of all files (without directories) within under the given {@code path} * @param path the path to walk * @param depth the maximum depth of subdirectories to search from within {@code path}. 1 * indicates it should return * only the files directly in {@code path} and not search any of its subdirectories * @return a stream of file name -> file contents as a raw byte array * @throws IOException can be thrown by the call to {@link Files#find(Path, int, BiPredicate, * FileVisitOption...)} */ private static Stream> walkInputsInPath(Path path, int depth) throws IOException { // @ParameterTest automatically closes Streams and AutoCloseable instances. // noinspection resource return Files .find(path, depth, (fileOrDir, basicFileAttributes) -> !basicFileAttributes.isDirectory(), FileVisitOption.FOLLOW_LINKS) // JUnit identifies individual runs of a `@ParameterizedTest` via their invocation number. // In order to get reproducible behavior e.g. when trying to debug a particular input, all // inputs thus have to be provided in deterministic order. .sorted() .map(file -> new SimpleImmutableEntry<>( file.getFileName().toString(), readAllBytesUnchecked(file))); } private static byte[] readAllBytesUnchecked(Path path) { try { return Files.readAllBytes(path); } catch (IOException e) { throw new IllegalStateException(e); } } }