From 1e9875ddc21cf863d2321c2b3c370554c0eff9bf Mon Sep 17 00:00:00 2001 From: Brian Lewis Date: Thu, 5 Oct 2023 15:45:31 +0200 Subject: [PATCH] Cleanup and documentation --- .../src/test/java/com/example/BUILD.bazel | 47 +++++---- .../java/com/example/DictionaryFuzzTests.java | 1 - .../jazzer/junit/FuzzTestExecutor.java | 22 +---- .../jazzer/junit/FuzzerDictionary.java | 95 ++++++++++++++----- 4 files changed, 103 insertions(+), 62 deletions(-) diff --git a/examples/junit/src/test/java/com/example/BUILD.bazel b/examples/junit/src/test/java/com/example/BUILD.bazel index 7f585a065..91db7a702 100644 --- a/examples/junit/src/test/java/com/example/BUILD.bazel +++ b/examples/junit/src/test/java/com/example/BUILD.bazel @@ -254,27 +254,32 @@ java_fuzz_target_test( ], ) -java_fuzz_target_test( - name = "DictionaryFuzzTests", - srcs = ["DictionaryFuzzTests.java"], - allowed_findings = ["java.lang.Error"], - env = {"JAZZER_FUZZ": "1"}, - target_class = "com.example.DictionaryFuzzTests", - # target_method = "inlineTest", - # target_method = "fileTest", - target_method = "mixedTest", - verify_crash_reproducer = False, - runtime_deps = [ - ":junit_runtime", - ], - deps = [ - "//examples/junit/src/test/resources:example_dictionaries", - "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test", - "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test_executor", - "@maven//:org_junit_jupiter_junit_jupiter_api", - "@maven//:org_junit_jupiter_junit_jupiter_params", - ], -) +[ + java_fuzz_target_test( + name = "DictionaryFuzzTests_" + method, + srcs = ["DictionaryFuzzTests.java"], + allowed_findings = ["java.lang.Error"], + env = {"JAZZER_FUZZ": "1"}, + target_class = "com.example.DictionaryFuzzTests", + target_method = method, + verify_crash_reproducer = False, + runtime_deps = [ + ":junit_runtime", + ], + deps = [ + "//examples/junit/src/test/resources:example_dictionaries", + "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test", + "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test_executor", + "@maven//:org_junit_jupiter_junit_jupiter_api", + "@maven//:org_junit_jupiter_junit_jupiter_params", + ], + ) + for method in [ + "inlineTest", + "fileTest", + "mixedTest", + ] +] java_library( name = "junit_runtime", diff --git a/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java b/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java index e66409e3c..d383f5e53 100644 --- a/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java +++ b/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java @@ -61,6 +61,5 @@ public void mixedTest(FuzzedDataProvider data) throws NoSuchAlgorithmException { if (MessageDigest.isEqual(hash, FLAG_SHA256)) { throw new Error("error found"); } - throw new RuntimeException("asdf"); } } diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java index cd25d29ad..d3fae26c0 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -27,7 +27,6 @@ import com.code_intelligence.jazzer.driver.junit.ExitCodeException; import com.code_intelligence.jazzer.junit.FuzzerDictionary.WithDictionary; import com.code_intelligence.jazzer.junit.FuzzerDictionary.WithDictionaryFile; -import com.code_intelligence.jazzer.utils.Log; import java.io.File; import java.io.IOException; import java.lang.reflect.Executable; @@ -124,13 +123,7 @@ public static FuzzTestExecutor prepare(ExtensionContext context, String maxDurat } Optional dictionary = createDictionaryFile(context); - if (dictionary.isPresent()) { - Log.info("fuzzing with dictionary " + dictionary.get()); - List lines = Files.readAllLines(Paths.get(dictionary.get())); - Log.info(String.join("%n", lines)); - - libFuzzerArgs.add("-dict=" + dictionary.get()); - } + dictionary.ifPresent(s -> libFuzzerArgs.add("-dict=" + s)); libFuzzerArgs.add("-max_total_time=" + durationStringToSeconds(maxDuration)); if (maxRuns > 0) { @@ -273,7 +266,8 @@ private static Path addInputAndSeedDirs( return javaSeedsDir; } - private static Optional createDictionaryFile(ExtensionContext context) { + private static Optional createDictionaryFile(ExtensionContext context) + throws IOException { List inlineDictionaries = AnnotationSupport.findRepeatableAnnotations( context.getRequiredTestMethod(), WithDictionary.class); @@ -282,15 +276,7 @@ private static Optional createDictionaryFile(ExtensionContext context) { AnnotationSupport.findRepeatableAnnotations( context.getRequiredTestMethod(), WithDictionaryFile.class); - try { - if (!inlineDictionaries.isEmpty() || !fileDictionaries.isEmpty()) { - return Optional.of(FuzzerDictionary.createMergedFile(inlineDictionaries, fileDictionaries)); - } else { - return Optional.empty(); - } - } catch (IOException e) { - throw new RuntimeException("error creating dictionary file", e); - } + return FuzzerDictionary.createDictionaryFile(inlineDictionaries, fileDictionaries); } /** Returns the list of arguments set on the command line. */ diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzerDictionary.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzerDictionary.java index b96c5d318..c3f2a78b9 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzerDictionary.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzerDictionary.java @@ -22,47 +22,91 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.platform.commons.util.ClassLoaderUtils; +/** + * Class that manages dictionaries for fuzz tests. The {@link WithDictionary} and {@link + * WithDictionaryFile} annotations are added to {@link FuzzTest}s to indicate that these + * dictionaries should be used for fuzzing this function. All tokens from all the sources will be + * added into a single merged dictionary file as libfuzzer can only accept a single {@code -dict} + * flag. + * + *

Syntax for dictionaries can be found here. + */ public class FuzzerDictionary { private static final String DICTIONARY_PREFIX = "jazzer-"; private static final String DICTIONARY_SUFFIX = ".dict"; - public static String createMergedFile(List inline, List files) - throws IOException { - // https://llvm.org/docs/LibFuzzer.html#dictionaries - Stream inlineTokens = - inline.stream() - .map(WithDictionary::tokens) - .flatMap(Arrays::stream) - .map((token) -> String.format("\"%s\"", token)); - Stream fileTokens = - files.stream() - .map(WithDictionaryFile::resourcePath) - .map(FuzzerDictionary::tokensFromFile) - .flatMap(List::stream); - Stream joined = Stream.concat(inlineTokens, fileTokens); + /** + * Create a temporary dictionary file for use during a fuzzing run based on the tokens found + * within {@code inline} and {@code files}. + * + * @param inline List of {@link WithDictionary} annotations that directly hold static token values + * to use in the dictionary + * @param files List of {@link WithDictionaryFile} annotations that reference dictionary files to + * include + * @return Optional containing the path to the created file, or nothing if {@code inline} and + * {@code files} are both empty + * @throws IOException + */ + public static Optional createDictionaryFile( + List inline, List files) throws IOException { + int sources = inline.size() + files.size(); + if (sources == 0) { + return Optional.empty(); + } + + Stream joined = Stream.concat(getInlineTokens(inline), getFileTokens(files)); File f = File.createTempFile(DICTIONARY_PREFIX, DICTIONARY_SUFFIX); f.deleteOnExit(); - - int sources = inline.size() + files.size(); Log.info(String.format("Creating merged dictionary from %d sources", sources)); try (OutputStream out = Files.newOutputStream(f.toPath())) { joined.forEach( (token) -> { try { + // the tokens will come in without newlines attached, so we append them here before + // writing String line = token.concat("\n"); out.write(line.getBytes()); } catch (IOException e) { - throw new RuntimeException("error writing to dictionary file"); + throw new UncheckedIOException(e); } }); } - return f.getPath(); + return Optional.of(f.getPath()); + } + + /** + * Gets the inlined arrays from each annotation, flattens them into a single stream, and wraps the + * elements in double quotes to comply with libfuzzer's dictionary syntax + * + * @param inline List of {@link WithDictionary} annotations to extract from + * @return stream of all the tokens from each of the elements of {@code inline} + */ + private static Stream getInlineTokens(List inline) { + return inline.stream() + .map(WithDictionary::tokens) + .flatMap(Arrays::stream) + .map((token) -> String.format("\"%s\"", token)); + } + + /** + * Gets the individual lines from each of the specified dictionary files + * + * @param files List of {@link WithDictionaryFile} annotations indicating which files to use + * @return stream of all lines from each of the files + */ + private static Stream getFileTokens(List files) { + return files.stream() + .map(WithDictionaryFile::resourcePath) + .map(FuzzerDictionary::tokensFromFile) + .flatMap(List::stream); } private static List tokensFromFile(String path) { @@ -71,15 +115,18 @@ private static List tokensFromFile(String path) { throw new FileNotFoundException(path); } BufferedReader reader = new BufferedReader(new InputStreamReader(resourceFile)); - // I think returning just reader.lines results in the file stream being closed before it's - // read so we immediately - // read the file and collect the lines into a list + // I think returning just reader.lines() results in the file stream being closed before it's + // read, so we immediately read the file and collect the lines into a list return reader.lines().collect(Collectors.toList()); } catch (IOException e) { - throw new RuntimeException("error reading dictionary file", e); + throw new UncheckedIOException(e); } } + /** + * Defines a dictionary where the values are given inline within this annotation. Values given + * here are intended to be bare tokens, so should not be given in the {@code name="value"} form. + */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(Dictionaries.class) @@ -87,6 +134,10 @@ private static List tokensFromFile(String path) { String[] tokens(); } + /** + * Defines a reference to a dictionary within the resources directory. These should follow libfuzzer's dictionary syntax. + */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(DictionaryFiles.class)