Skip to content

Commit 7453e9c

Browse files
committed
feat: ensure correct length constraints for char[] mutation
1 parent 5a3fdb9 commit 7453e9c

File tree

4 files changed

+86
-9
lines changed

4 files changed

+86
-9
lines changed

examples/junit/src/test/java/com/example/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,23 @@ java_fuzz_target_test(
224224
],
225225
)
226226

227+
java_fuzz_target_test(
228+
name = "CharArrayWithLengthFuzzTest",
229+
srcs = ["CharArrayWithLengthFuzzTest.java"],
230+
allowed_findings = ["java.lang.RuntimeException"],
231+
tags = ["no-jdk8"],
232+
target_class = "com.example.CharArrayWithLengthFuzzTest",
233+
verify_crash_reproducer = False,
234+
runtime_deps = [
235+
":junit_runtime",
236+
],
237+
deps = [
238+
"//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test",
239+
"@maven//:org_junit_jupiter_junit_jupiter_api",
240+
"@maven//:org_mockito_mockito_core",
241+
],
242+
)
243+
227244
java_fuzz_target_test(
228245
name = "MutatorFuzzTest",
229246
srcs = ["MutatorFuzzTest.java"],
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2025 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 com.code_intelligence.jazzer.junit.FuzzTest;
20+
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
21+
import com.code_intelligence.jazzer.mutation.annotation.WithLength;
22+
import java.nio.charset.Charset;
23+
24+
public class CharArrayWithLengthFuzzTest {
25+
@FuzzTest
26+
public void fuzzCharArray(char @NotNull @WithLength(max = 5) [] data) {
27+
String expression = new String(data);
28+
// Each '中' character is encoded using three bytes with CESU8. To satisfy this check, the
29+
// underlying CESU8-encoded byte array should have at least 15 bytes.
30+
if (expression.equals("中中中中中")) {
31+
assert expression.getBytes(Charset.forName("CESU-8")).length == 15;
32+
throw new RuntimeException("Found evil code");
33+
}
34+
}
35+
}

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

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import java.lang.reflect.AnnotatedType;
4444
import java.nio.ByteBuffer;
4545
import java.nio.charset.Charset;
46+
import java.util.Arrays;
4647
import java.util.Optional;
4748
import java.util.function.BiFunction;
4849
import java.util.function.Function;
@@ -75,6 +76,8 @@ public static final class PrimitiveArrayMutator<T> extends SerializingMutator<T>
7576
private static final Charset FUZZED_DATA_CHARSET = Charset.forName("CESU-8");
7677
private long minRange;
7778
private long maxRange;
79+
private int minLength;
80+
private int maxLength;
7881
private boolean allowNaN;
7982
private float minFloatRange;
8083
private float maxFloatRange;
@@ -90,6 +93,7 @@ public static final class PrimitiveArrayMutator<T> extends SerializingMutator<T>
9093
public PrimitiveArrayMutator(AnnotatedType type) {
9194
elementType = ((AnnotatedArrayType) type).getAnnotatedGenericComponentType();
9295
extractRange(elementType);
96+
extractLength(type);
9397
AnnotatedType innerByteArray =
9498
forwardAnnotations(
9599
type, convertWithLength(type, new TypeHolder<byte[]>() {}.annotatedType()));
@@ -209,11 +213,14 @@ private void extractRange(AnnotatedType type) {
209213
}
210214
}
211215

212-
private static AnnotatedType convertWithLength(AnnotatedType type, AnnotatedType newType) {
213-
AnnotatedType elementType = ((AnnotatedArrayType) type).getAnnotatedGenericComponentType();
216+
private void extractLength(AnnotatedType type) {
214217
Optional<WithLength> withLength = Optional.ofNullable(type.getAnnotation(WithLength.class));
215-
int minLength = withLength.map(WithLength::min).orElse(DEFAULT_MIN_LENGTH);
216-
int maxLength = withLength.map(WithLength::max).orElse(DEFAULT_MAX_LENGTH);
218+
minLength = withLength.map(WithLength::min).orElse(DEFAULT_MIN_LENGTH);
219+
maxLength = withLength.map(WithLength::max).orElse(DEFAULT_MAX_LENGTH);
220+
}
221+
222+
private AnnotatedType convertWithLength(AnnotatedType type, AnnotatedType newType) {
223+
AnnotatedType elementType = ((AnnotatedArrayType) type).getAnnotatedGenericComponentType();
217224
switch (elementType.getType().getTypeName()) {
218225
case "int":
219226
case "float":
@@ -222,8 +229,11 @@ private static AnnotatedType convertWithLength(AnnotatedType type, AnnotatedType
222229
case "double":
223230
return withLength(newType, minLength * 8, maxLength * 8);
224231
case "short":
225-
case "char":
226232
return withLength(newType, minLength * 2, maxLength * 2);
233+
case "char":
234+
// CESU-8 encodes each UTF-16 char (including surrogates) as at most 3 bytes.
235+
// So a char[] of length n needs at most 3 * n bytes.
236+
return withLength(newType, minLength * 3, maxLength * 3);
227237
case "boolean":
228238
case "byte":
229239
return withLength(newType, minLength, maxLength);
@@ -241,7 +251,7 @@ private static AnnotatedType convertWithLength(AnnotatedType type, AnnotatedType
241251
case "short":
242252
return getShortPrimitiveArray(minRange, maxRange);
243253
case "char":
244-
return getCharPrimitiveArray(minRange, maxRange);
254+
return getCharPrimitiveArray(minRange, maxRange, minLength, maxLength);
245255
case "float":
246256
return getFloatPrimitiveArray(minFloatRange, maxFloatRange, allowNaN);
247257
case "double":
@@ -263,9 +273,16 @@ public char[] postMutateChars(byte[] bytes, PseudoRandom prng) {
263273
return (char[]) toPrimitive.apply(bytes);
264274
} else {
265275
char[] chars = new String(bytes, FUZZED_DATA_CHARSET).toCharArray();
276+
266277
for (int i = 0; i < chars.length; i++) {
267278
chars[i] = (char) forceInRange(chars[i], minRange, maxRange);
268279
}
280+
281+
if (chars.length < minLength) {
282+
return Arrays.copyOf(chars, minLength);
283+
} else if (chars.length > maxLength) {
284+
return Arrays.copyOf(chars, maxLength);
285+
}
269286
return chars;
270287
}
271288
}
@@ -407,10 +424,18 @@ public static Function<byte[], short[]> getShortPrimitiveArray(long minRange, lo
407424
};
408425
}
409426

410-
public static Function<byte[], char[]> getCharPrimitiveArray(long minRange, long maxRange) {
427+
public static Function<byte[], char[]> getCharPrimitiveArray(
428+
long minRange, long maxRange, int minLength, int maxLength) {
411429
int nBytes = 2;
412430
return (byte[] byteArray) -> {
413431
if (byteArray == null) return null;
432+
433+
if (byteArray.length < minLength * 2) {
434+
byteArray = Arrays.copyOf(byteArray, minLength * 2);
435+
} else if (byteArray.length > maxLength * 2) {
436+
byteArray = Arrays.copyOf(byteArray, maxLength * 2);
437+
}
438+
414439
char extraBytes = (char) (byteArray.length % nBytes);
415440
char[] result = new char[byteArray.length / nBytes + (extraBytes > 0 ? 1 : 0)];
416441
ByteBuffer buffer = ByteBuffer.wrap(byteArray);

src/test/java/com/code_intelligence/jazzer/mutation/mutator/lang/PrimitiveArrayMutatorTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public class PrimitiveArrayMutatorTest {
8686
static Function<char[], byte[]> charsToBytes =
8787
(Function<char[], byte[]>) makePrimitiveArrayToBytesConverter(annotatedType_char);
8888
static Function<byte[], char[]> bytesToChars =
89-
getCharPrimitiveArray(Character.MIN_VALUE, Character.MAX_VALUE);
89+
getCharPrimitiveArray(Character.MIN_VALUE, Character.MAX_VALUE, 0, 1000);
9090

9191
static Function<boolean[], byte[]> booleansToBytes =
9292
(Function<boolean[], byte[]>) makePrimitiveArrayToBytesConverter(annotatedType_boolean);
@@ -305,7 +305,7 @@ static Stream<Arguments> bytes2charsTestCases() {
305305
@ParameterizedTest
306306
@MethodSource("bytes2charsTestCases")
307307
void testArrayConversion_bytes2chars(byte[] input, char[] expected, long min, long max) {
308-
Function<byte[], char[]> fn = getCharPrimitiveArray(min, max);
308+
Function<byte[], char[]> fn = getCharPrimitiveArray(min, max, 0, 100);
309309
assertThat(fn.apply(input)).isEqualTo(expected);
310310
}
311311

0 commit comments

Comments
 (0)