diff --git a/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java b/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java
index fca414f5e..dde2d8238 100644
--- a/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java
+++ b/examples/junit/src/test/java/com/example/DictionaryFuzzTests.java
@@ -27,12 +27,12 @@
public class DictionaryFuzzTests {
// Generated via:
- // printf 'a_53Cr3T_fl4G' | openssl dgst -binary -sha256 | openssl base64 -A
+ // printf 'a_53Cr"3T_fl4G' | openssl dgst -binary -sha256 | openssl base64 -A
// Luckily the fuzzer can't read comments ;-)
private static final byte[] FLAG_SHA256 =
- Base64.getDecoder().decode("IT7goSzYg6MXLugHl9H4oCswA+OEb4bGZmKrDzlZjO4=");
+ Base64.getDecoder().decode("vCLInoVuMxJonT4UKjsMl0LPXTowkYS7t0uBpw0pRo8=");
- @DictionaryEntries(tokens = {"a_", "53Cr3T_", "fl4G"})
+ @DictionaryEntries(tokens = {"a_", "53Cr\"3T_", "fl4G"})
@FuzzTest
public void inlineTest(FuzzedDataProvider data)
throws NoSuchAlgorithmException, TestSuccessfulException {
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel
index ba374137a..0e825fe37 100644
--- a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel
+++ b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel
@@ -37,6 +37,7 @@ java_library(
visibility = [
"//examples/junit/src/test/java/com/example:__pkg__",
"//selffuzz/src/test/java/com/code_intelligence/selffuzz:__subpackages__",
+ "//src/test/java/com/code_intelligence/jazzer/junit:__pkg__",
],
exports = [
":lifecycle",
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/DictionaryEntries.java b/src/main/java/com/code_intelligence/jazzer/junit/DictionaryEntries.java
index 16695cf94..cad4f2e58 100644
--- a/src/main/java/com/code_intelligence/jazzer/junit/DictionaryEntries.java
+++ b/src/main/java/com/code_intelligence/jazzer/junit/DictionaryEntries.java
@@ -23,12 +23,17 @@
import java.lang.annotation.Target;
/**
- * Defines a reference to a dictionary within the resources directory. These should follow libfuzzer's dictionary syntax.
+ * Adds the given strings to the fuzzer's dictionary. This is particularly useful for adding tokens
+ * that have special meaning in the context of your fuzz test, but are difficult for the fuzzer to
+ * discover automatically.
+ *
+ *
Typical examples include valid credentials for mock accounts in a web application or a
+ * collection of valid HTML tags for an HTML parser.
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(DictionaryEntriesList.class)
public @interface DictionaryEntries {
+ /** Individual strings to add to the fuzzer dictionary. */
String[] tokens();
}
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 307a07caa..0016a934b 100644
--- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzerDictionary.java
+++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzerDictionary.java
@@ -32,6 +32,7 @@
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.platform.commons.support.AnnotationSupport;
@@ -118,7 +119,41 @@ private static Stream getInlineTokens(List inline) {
return inline.stream()
.map(DictionaryEntries::tokens)
.flatMap(Arrays::stream)
- .map(token -> String.format("\"%s\"", token));
+ .map(FuzzerDictionary::escapeForDictionary);
+ }
+
+ static String escapeForDictionary(String rawString) {
+ // https://llvm.org/docs/LibFuzzer.html#dictionaries
+ String escapedString =
+ // libFuzzer reads raw byte strings and assumes that every non-printable, non-space
+ // character is escaped. Since our fuzzer generates UTF-8 strings, we decode the string with
+ // UTF-8 and encode it to ISO-8859-1 (aka Latin-1), which results in a string with one byte
+ // characters representing the UTF-8 encoded bytes.
+ new String(rawString.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)
+ .chars()
+ .flatMap(FuzzerDictionary::escapeByteForDictionary)
+ .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+ .toString();
+ return '"' + escapedString + '"';
+ }
+
+ private static IntStream escapeByteForDictionary(int c) {
+ // Escape all characters that are not printable ASCII or whitespace as well as the backslash
+ // and double quote characters.
+ // https://github.com/llvm/llvm-project/blob/675231eb09ca37a8b76f748c0b73a1e26604ff20/compiler-rt/lib/fuzzer/FuzzerUtil.cpp#L81
+ if (c == '\\') {
+ return IntStream.of('\\', '\\');
+ } else if (c == '\"') {
+ return IntStream.of('\\', '\"');
+ } else if ((c < 32 && !Character.isWhitespace(c)) || c > 127) {
+ return IntStream.of(
+ '\\',
+ 'x',
+ Character.toUpperCase(Character.forDigit(c >> 4, 16)),
+ Character.toUpperCase(Character.forDigit(c & 0x0F, 16)));
+ } else {
+ return IntStream.of(c);
+ }
}
/**
diff --git a/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel
index d9591fab9..8691ebd77 100644
--- a/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel
+++ b/src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel
@@ -22,6 +22,17 @@ java_junit5_test(
],
)
+java_junit5_test(
+ name = "FuzzerDictionaryTest",
+ size = "small",
+ srcs = ["FuzzerDictionaryTest.java"],
+ deps = JUNIT5_DEPS + [
+ "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test",
+ "@maven//:com_google_truth_truth",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ ],
+)
+
java_test(
name = "RegressionTestTest",
srcs = ["RegressionTestTest.java"],
diff --git a/src/test/java/com/code_intelligence/jazzer/junit/FuzzerDictionaryTest.java b/src/test/java/com/code_intelligence/jazzer/junit/FuzzerDictionaryTest.java
new file mode 100644
index 000000000..2b7ad2aac
--- /dev/null
+++ b/src/test/java/com/code_intelligence/jazzer/junit/FuzzerDictionaryTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 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.FuzzerDictionary.escapeForDictionary;
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class FuzzerDictionaryTest {
+ @Test
+ void testEscapeForDictionary() {
+ assertThat(escapeForDictionary("foo")).isEqualTo("\"foo\"");
+ assertThat(escapeForDictionary("f\"o\\o\tbar")).isEqualTo("\"f\\\"o\\\\o\tbar\"");
+ assertThat(escapeForDictionary("\u0012\u001A")).isEqualTo("\"\\x12\\x1A\"");
+ assertThat(escapeForDictionary("✂\uD83D\uDCCB"))
+ .isEqualTo("\"\\xE2\\x9C\\x82\\xF0\\x9F\\x93\\x8B\"");
+ }
+}