Skip to content

Commit 5eac7ec

Browse files
committed
feat: String mutator now uses @DictionaryProvider
The mutator now extracts applicable Strings from the provider and uses them during mutation according to the pInv of the last @DictionaryProvider annotation it found on this type.
1 parent 500a15d commit 5eac7ec

File tree

4 files changed

+272
-2
lines changed

4 files changed

+272
-2
lines changed

src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,31 @@
1717
package com.code_intelligence.jazzer.mutation.mutator.lang;
1818

1919
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable;
20-
import static com.code_intelligence.jazzer.mutation.support.TypeSupport.*;
20+
import static com.code_intelligence.jazzer.mutation.support.DictionaryProviderSupport.extractFirstInvProbability;
21+
import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass;
22+
import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull;
23+
import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withLength;
2124

2225
import com.code_intelligence.jazzer.mutation.annotation.Ascii;
2326
import com.code_intelligence.jazzer.mutation.annotation.UrlSegment;
2427
import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length;
2528
import com.code_intelligence.jazzer.mutation.api.Debuggable;
2629
import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory;
2730
import com.code_intelligence.jazzer.mutation.api.MutatorFactory;
31+
import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
2832
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
2933
import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutatorFactory;
3034
import com.code_intelligence.jazzer.mutation.runtime.MutatorRuntime;
35+
import com.code_intelligence.jazzer.mutation.support.DictionaryProviderSupport;
3136
import com.code_intelligence.jazzer.mutation.support.TypeHolder;
37+
import java.io.DataInputStream;
38+
import java.io.DataOutputStream;
39+
import java.io.IOException;
3240
import java.lang.reflect.AnnotatedType;
3341
import java.nio.charset.StandardCharsets;
3442
import java.util.Optional;
3543
import java.util.function.Predicate;
44+
import java.util.stream.Stream;
3645

3746
final class StringMutatorFactory implements MutatorFactory {
3847
private static final int HEADER_MASK = 0b1100_0000;
@@ -177,7 +186,9 @@ public Optional<SerializingMutator<?>> tryCreate(
177186

178187
AnnotatedType innerByteArray =
179188
notNull(withLength(new TypeHolder<byte[]>() {}.annotatedType(), min, max));
180-
return LibFuzzerMutatorFactory.tryCreate(runtime, innerByteArray);
189+
Optional<SerializingMutator<?>> innerMutator =
190+
LibFuzzerMutatorFactory.tryCreate(runtime, innerByteArray);
191+
return UserDictionaryMutatorWrapper.of(runtime, innerMutator, type, min, max);
181192
})
182193
.map(
183194
byteArrayMutator -> {
@@ -199,4 +210,102 @@ public Optional<SerializingMutator<?>> tryCreate(
199210
(Predicate<Debuggable> inCycle) -> "String");
200211
});
201212
}
213+
214+
private static final class UserDictionaryMutatorWrapper extends SerializingMutator<byte[]> {
215+
private final byte[][] dictionaryValues;
216+
private final SerializingMutator<byte[]> basicMutator;
217+
private final int pInv;
218+
219+
public static Optional<SerializingMutator<?>> of(
220+
MutatorRuntime runtime,
221+
Optional<SerializingMutator<?>> mutator,
222+
AnnotatedType type,
223+
int minSize,
224+
int maxSize) {
225+
if (!mutator.isPresent()) {
226+
return Optional.empty();
227+
}
228+
SerializingMutator<byte[]> castedMutator =
229+
mutator.map(m -> (SerializingMutator<byte[]>) m).get();
230+
Optional<byte[][]> values = generateDictionaryValues(runtime, type, minSize, maxSize);
231+
if (!values.isPresent()) {
232+
return mutator;
233+
}
234+
return Optional.of(
235+
new UserDictionaryMutatorWrapper(
236+
castedMutator, values.get(), extractFirstInvProbability(type)));
237+
}
238+
239+
public UserDictionaryMutatorWrapper(
240+
SerializingMutator<byte[]> basicMutator, byte[][] dictionaryValues, int pInv) {
241+
this.basicMutator = basicMutator;
242+
this.dictionaryValues = dictionaryValues;
243+
this.pInv = pInv;
244+
}
245+
246+
public static Optional<byte[][]> generateDictionaryValues(
247+
MutatorRuntime runtime, AnnotatedType type, int minSize, int maxSize) {
248+
return DictionaryProviderSupport.extractProviderStreams(runtime, type)
249+
.map(
250+
stream ->
251+
stream
252+
.flatMap(
253+
o -> {
254+
if (o instanceof String) {
255+
return Stream.of(((String) o).getBytes(StandardCharsets.UTF_8));
256+
} else {
257+
return Stream.empty();
258+
}
259+
})
260+
.filter(b -> b.length >= minSize && b.length <= maxSize)
261+
.distinct()
262+
.toArray(byte[][]::new));
263+
}
264+
265+
@Override
266+
public String toDebugString(Predicate<Debuggable> isInCycle) {
267+
return "String";
268+
}
269+
270+
@Override
271+
public byte[] read(DataInputStream in) throws IOException {
272+
return basicMutator.read(in);
273+
}
274+
275+
@Override
276+
public void write(byte[] value, DataOutputStream out) throws IOException {
277+
basicMutator.write(value, out);
278+
}
279+
280+
@Override
281+
public byte[] detach(byte[] value) {
282+
return basicMutator.detach(value);
283+
}
284+
285+
@Override
286+
public byte[] init(PseudoRandom prng) {
287+
if (prng.trueInOneOutOf(pInv)) {
288+
return prng.pickIn(dictionaryValues);
289+
}
290+
return basicMutator.init(prng);
291+
}
292+
293+
@Override
294+
public byte[] mutate(byte[] value, PseudoRandom prng) {
295+
if (prng.trueInOneOutOf(pInv)) {
296+
return prng.pickIn(dictionaryValues);
297+
}
298+
return basicMutator.mutate(value, prng);
299+
}
300+
301+
@Override
302+
public byte[] crossOver(byte[] value, byte[] otherValue, PseudoRandom prng) {
303+
return basicMutator.crossOver(value, otherValue, prng);
304+
}
305+
306+
@Override
307+
public boolean hasFixedSize() {
308+
return false;
309+
}
310+
}
202311
}

src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ java_library(
77
],
88
deps = [
99
"//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
10+
"//src/main/java/com/code_intelligence/jazzer/mutation/runtime",
1011
"//src/main/java/com/code_intelligence/jazzer/mutation/utils",
1112
"//src/main/java/com/code_intelligence/jazzer/utils:log",
1213
],

tests/BUILD.bazel

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,28 @@ java_fuzz_target_test(
2020
verify_crash_input = False,
2121
)
2222

23+
java_fuzz_target_test(
24+
name = "DictionaryProviderFuzzerLongString",
25+
srcs = [
26+
"src/test/java/com/example/DictionaryProviderFuzzerLongString.java",
27+
],
28+
allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"],
29+
fuzzer_args = [
30+
"-runs=10000",
31+
],
32+
target_class = "com.example.DictionaryProviderFuzzerLongString",
33+
verify_crash_input = False,
34+
verify_crash_reproducer = False,
35+
deps = [
36+
"//deploy:jazzer-junit",
37+
"//deploy:jazzer-project",
38+
"@maven//:com_google_truth_truth",
39+
"@maven//:org_junit_jupiter_junit_jupiter_api",
40+
"@maven//:org_junit_jupiter_junit_jupiter_engine",
41+
"@maven//:org_junit_platform_junit_platform_launcher",
42+
],
43+
)
44+
2345
java_fuzz_target_test(
2446
name = "JpegImageParserAutofuzz",
2547
allowed_findings = ["java.lang.NegativeArraySizeException"],
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2024 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.example;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow;
22+
import com.code_intelligence.jazzer.junit.FuzzTest;
23+
import com.code_intelligence.jazzer.mutation.annotation.DictionaryProvider;
24+
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
25+
import com.code_intelligence.jazzer.mutation.annotation.WithSize;
26+
import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length;
27+
import java.util.List;
28+
import java.util.stream.Stream;
29+
30+
public class DictionaryProviderFuzzerLongString {
31+
private static final String str00 = repeat("0123456789abcdef", 50);
32+
private static final String str01 = repeat("sitting duck suprime", 53);
33+
private static final String str10 = repeat("poa0189fbhBHOVBO781%", 30);
34+
private static final String unused = repeat("XdeadbeefX", 21);
35+
36+
public static Stream<?> dict0() {
37+
return Stream.of(
38+
str00,
39+
str01,
40+
// We can mix all kinds of values in the same dictionary.
41+
// Each mutator only takes the values it can use.
42+
123,
43+
4567899999L);
44+
}
45+
46+
public static Stream<?> dict1() {
47+
return Stream.of(str10);
48+
}
49+
50+
public static Stream<?> emptyDict() {
51+
return Stream.of();
52+
}
53+
54+
public static Stream<?> unusedDictionary() {
55+
return Stream.of(unused);
56+
}
57+
58+
@FuzzTest
59+
// Just propagate the dictionary to all types of the fuzz test method that can use it.
60+
// Annotating individual String parameters is also possible.
61+
@DictionaryProvider(
62+
value = {"dict0"},
63+
// Here we use a very low probability for picking dictionary values.
64+
// It gets overwritten for some arguments below.
65+
pInv = 1000000000)
66+
public static void fuzzerTestOneInput(
67+
@NotNull
68+
// Extend the maximum length of the String so that the dictionary values can actually be
69+
// used
70+
@WithUtf8Length(max = 10000)
71+
// The String mutator for this argument will use "dict1" and "emptyDict" with pInv = 2
72+
// for all dictionary entries.
73+
@DictionaryProvider(
74+
value = {"emptyDict"},
75+
// Set pInv = 2 for the String mutator
76+
pInv = 2)
77+
String data00,
78+
79+
// Identical annotations as for data00
80+
@NotNull
81+
@WithUtf8Length(max = 10000)
82+
@DictionaryProvider(
83+
value = {"emptyDict"},
84+
pInv = 2)
85+
String data01,
86+
87+
// The String mutator, inside the List mutator for this argument will use "dict0" and
88+
// "dict1" with pInv = 2 for all dictionary entries.
89+
// Note that the String mutator is not directly annotated, and gets annotated because
90+
// @DictionaryProvider has PropertyConstraint.RECURSIVE
91+
@DictionaryProvider(
92+
value = {"dict1"},
93+
pInv = 2)
94+
@NotNull
95+
@WithSize(max = 2)
96+
List<@NotNull String> data1,
97+
98+
// The String mutator for this argument will use entries from
99+
// @DictionaryProvider(value={"dict0"}, pInv = 1000000000), that get propagated here from the
100+
// method annotation.
101+
@NotNull String data2) {
102+
103+
// This should only happen 2:1000000000 times.
104+
assertThat(data2.equals(str00)).isFalse();
105+
assertThat(data2.equals(str01)).isFalse();
106+
107+
// Error: matched a long string from dictionary entry this variable was was NOT annotated with.
108+
// This should never happen.
109+
assertThat(data00.equals(str10)).isFalse();
110+
assertThat(data00.equals(unused)).isFalse();
111+
assertThat(data01.equals(str10)).isFalse();
112+
assertThat(data01.equals(unused)).isFalse();
113+
assertThat(data1.equals(unused)).isFalse();
114+
assertThat(data2.equals(str10)).isFalse();
115+
assertThat(data2.equals(unused)).isFalse();
116+
117+
/*
118+
* libFuzzer's table of recent compares only allows 64 bytes, so asking the fuzzer to construct
119+
* these long strings would run for a very very long time without finding them. However, with a
120+
* @DictionaryProvider this problem is trivial, because we can directly provide these long strings to
121+
* the fuzzer, and also force that they are used more often by setting pInv to a low value close to 2.
122+
*/
123+
if (data00.equals(str00)
124+
&& data01.equals(str01)
125+
&& !data1.isEmpty()
126+
&& data1.get(0).equals(str10)) {
127+
throw new FuzzerSecurityIssueLow("Found all long strings as expected");
128+
}
129+
}
130+
131+
private static String repeat(String str, int count) {
132+
StringBuilder sb = new StringBuilder(str.length() * count);
133+
for (int i = 0; i < count; i++) {
134+
sb.append(str);
135+
}
136+
return sb.toString();
137+
}
138+
}

0 commit comments

Comments
 (0)