Skip to content

Commit b0bcdc1

Browse files
committed
junit: Automatically escape @DictionaryEntries
1 parent fdaf401 commit b0bcdc1

File tree

6 files changed

+91
-6
lines changed

6 files changed

+91
-6
lines changed

examples/junit/src/test/java/com/example/DictionaryFuzzTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727

2828
public class DictionaryFuzzTests {
2929
// Generated via:
30-
// printf 'a_53Cr3T_fl4G' | openssl dgst -binary -sha256 | openssl base64 -A
30+
// printf 'a_53Cr"3T_fl4G' | openssl dgst -binary -sha256 | openssl base64 -A
3131
// Luckily the fuzzer can't read comments ;-)
3232
private static final byte[] FLAG_SHA256 =
33-
Base64.getDecoder().decode("IT7goSzYg6MXLugHl9H4oCswA+OEb4bGZmKrDzlZjO4=");
33+
Base64.getDecoder().decode("vCLInoVuMxJonT4UKjsMl0LPXTowkYS7t0uBpw0pRo8=");
3434

35-
@DictionaryEntries(tokens = {"a_", "53Cr3T_", "fl4G"})
35+
@DictionaryEntries(tokens = {"a_", "53Cr\"3T_", "fl4G"})
3636
@FuzzTest
3737
public void inlineTest(FuzzedDataProvider data)
3838
throws NoSuchAlgorithmException, TestSuccessfulException {

src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ java_library(
3737
visibility = [
3838
"//examples/junit/src/test/java/com/example:__pkg__",
3939
"//selffuzz/src/test/java/com/code_intelligence/selffuzz:__subpackages__",
40+
"//src/test/java/com/code_intelligence/jazzer/junit:__pkg__",
4041
],
4142
exports = [
4243
":lifecycle",

src/main/java/com/code_intelligence/jazzer/junit/DictionaryEntries.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@
2323
import java.lang.annotation.Target;
2424

2525
/**
26-
* Defines a reference to a dictionary within the resources directory. These should follow <a
27-
* href="https://llvm.org/docs/LibFuzzer.html#dictionaries">libfuzzer's dictionary syntax</a>.
26+
* Adds the given strings to the fuzzer's dictionary. This is particularly useful for adding tokens
27+
* that have special meaning in the context of your fuzz test, but are difficult for the fuzzer to
28+
* discover automatically.
29+
*
30+
* <p>Typical examples include valid credentials for mock accounts in a web application or a
31+
* collection of valid HTML tags for an HTML parser.
2832
*/
2933
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
3034
@Retention(RetentionPolicy.RUNTIME)
3135
@Repeatable(DictionaryEntriesList.class)
3236
public @interface DictionaryEntries {
37+
/** Individual strings to add to the fuzzer dictionary. */
3338
String[] tokens();
3439
}

src/main/java/com/code_intelligence/jazzer/junit/FuzzerDictionary.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.List;
3333
import java.util.Optional;
3434
import java.util.stream.Collectors;
35+
import java.util.stream.IntStream;
3536
import java.util.stream.Stream;
3637
import org.junit.platform.commons.support.AnnotationSupport;
3738

@@ -118,7 +119,41 @@ private static Stream<String> getInlineTokens(List<DictionaryEntries> inline) {
118119
return inline.stream()
119120
.map(DictionaryEntries::tokens)
120121
.flatMap(Arrays::stream)
121-
.map(token -> String.format("\"%s\"", token));
122+
.map(FuzzerDictionary::escapeForDictionary);
123+
}
124+
125+
static String escapeForDictionary(String rawString) {
126+
// https://llvm.org/docs/LibFuzzer.html#dictionaries
127+
String escapedString =
128+
// libFuzzer reads raw byte strings and assumes that every non-printable, non-space
129+
// character is escaped. Since our fuzzer generates UTF-8 strings, we decode the string with
130+
// UTF-8 and encode it to ISO-8859-1 (aka Latin-1), which results in a string with one byte
131+
// characters representing the UTF-8 encoded bytes.
132+
new String(rawString.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)
133+
.chars()
134+
.flatMap(FuzzerDictionary::escapeByteForDictionary)
135+
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
136+
.toString();
137+
return '"' + escapedString + '"';
138+
}
139+
140+
private static IntStream escapeByteForDictionary(int c) {
141+
// Escape all characters that are not printable ASCII or whitespace as well as the backslash
142+
// and double quote characters.
143+
// https://github.com/llvm/llvm-project/blob/675231eb09ca37a8b76f748c0b73a1e26604ff20/compiler-rt/lib/fuzzer/FuzzerUtil.cpp#L81
144+
if (c == '\\') {
145+
return IntStream.of('\\', '\\');
146+
} else if (c == '\"') {
147+
return IntStream.of('\\', '\"');
148+
} else if ((c < 32 && !Character.isWhitespace(c)) || c > 127) {
149+
return IntStream.of(
150+
'\\',
151+
'x',
152+
Character.toUpperCase(Character.forDigit(c >> 4, 16)),
153+
Character.toUpperCase(Character.forDigit(c & 0x0F, 16)));
154+
} else {
155+
return IntStream.of(c);
156+
}
122157
}
123158

124159
/**

src/test/java/com/code_intelligence/jazzer/junit/BUILD.bazel

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ java_junit5_test(
2222
],
2323
)
2424

25+
java_junit5_test(
26+
name = "FuzzerDictionaryTest",
27+
size = "small",
28+
srcs = ["FuzzerDictionaryTest.java"],
29+
deps = JUNIT5_DEPS + [
30+
"//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test",
31+
"@maven//:com_google_truth_truth",
32+
"@maven//:org_junit_jupiter_junit_jupiter_api",
33+
],
34+
)
35+
2536
java_test(
2637
name = "RegressionTestTest",
2738
srcs = ["RegressionTestTest.java"],
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2023 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.junit;
18+
19+
import static com.code_intelligence.jazzer.junit.FuzzerDictionary.escapeForDictionary;
20+
import static com.google.common.truth.Truth.assertThat;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
class FuzzerDictionaryTest {
25+
@Test
26+
void testEscapeForDictionary() {
27+
assertThat(escapeForDictionary("foo")).isEqualTo("\"foo\"");
28+
assertThat(escapeForDictionary("f\"o\\o\tbar")).isEqualTo("\"f\\\"o\\\\o\tbar\"");
29+
assertThat(escapeForDictionary("\u0012\u001A")).isEqualTo("\"\\x12\\x1A\"");
30+
assertThat(escapeForDictionary("✂\uD83D\uDCCB"))
31+
.isEqualTo("\"\\xE2\\x9C\\x82\\xF0\\x9F\\x93\\x8B\"");
32+
}
33+
}

0 commit comments

Comments
 (0)