Skip to content

Commit ef7ea96

Browse files
committed
feat: ensure correct length constraints for char[] mutation
1 parent 96e19d6 commit ef7ea96

File tree

4 files changed

+89
-9
lines changed

4 files changed

+89
-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: 35 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,15 @@ 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+
withLength.ifPresent(System.err::println);
219+
minLength = withLength.map(WithLength::min).orElse(DEFAULT_MIN_LENGTH);
220+
maxLength = withLength.map(WithLength::max).orElse(DEFAULT_MAX_LENGTH);
221+
}
222+
223+
private AnnotatedType convertWithLength(AnnotatedType type, AnnotatedType newType) {
224+
AnnotatedType elementType = ((AnnotatedArrayType) type).getAnnotatedGenericComponentType();
217225
switch (elementType.getType().getTypeName()) {
218226
case "int":
219227
case "float":
@@ -222,8 +230,13 @@ private static AnnotatedType convertWithLength(AnnotatedType type, AnnotatedType
222230
case "double":
223231
return withLength(newType, minLength * 8, maxLength * 8);
224232
case "short":
225-
case "char":
226233
return withLength(newType, minLength * 2, maxLength * 2);
234+
case "char":
235+
// CESU-8 encoding uses at maximum 6 bytes to encode a character. This value represents
236+
// the maximum size needed for the underlying byte array to hold the corresponding
237+
// char array with the specified length range. After the conversion to a char array in the
238+
// mutator, we should ensure the exact length constraints
239+
return withLength(newType, minLength * 6, maxLength * 6);
227240
case "boolean":
228241
case "byte":
229242
return withLength(newType, minLength, maxLength);
@@ -241,7 +254,7 @@ private static AnnotatedType convertWithLength(AnnotatedType type, AnnotatedType
241254
case "short":
242255
return getShortPrimitiveArray(minRange, maxRange);
243256
case "char":
244-
return getCharPrimitiveArray(minRange, maxRange);
257+
return getCharPrimitiveArray(minRange, maxRange, minLength, maxLength);
245258
case "float":
246259
return getFloatPrimitiveArray(minFloatRange, maxFloatRange, allowNaN);
247260
case "double":
@@ -263,9 +276,16 @@ public char[] postMutateChars(byte[] bytes, PseudoRandom prng) {
263276
return (char[]) toPrimitive.apply(bytes);
264277
} else {
265278
char[] chars = new String(bytes, FUZZED_DATA_CHARSET).toCharArray();
279+
266280
for (int i = 0; i < chars.length; i++) {
267281
chars[i] = (char) forceInRange(chars[i], minRange, maxRange);
268282
}
283+
284+
if (chars.length < minLength) {
285+
return Arrays.copyOf(chars, minLength);
286+
} else if (chars.length > maxLength) {
287+
return Arrays.copyOf(chars, maxLength);
288+
}
269289
return chars;
270290
}
271291
}
@@ -407,10 +427,18 @@ public static Function<byte[], short[]> getShortPrimitiveArray(long minRange, lo
407427
};
408428
}
409429

410-
public static Function<byte[], char[]> getCharPrimitiveArray(long minRange, long maxRange) {
430+
public static Function<byte[], char[]> getCharPrimitiveArray(
431+
long minRange, long maxRange, int minLength, int maxLength) {
411432
int nBytes = 2;
412433
return (byte[] byteArray) -> {
413434
if (byteArray == null) return null;
435+
436+
if (byteArray.length < minLength * 2) {
437+
byteArray = Arrays.copyOf(byteArray, minLength * 2);
438+
} else if (byteArray.length > maxLength * 2) {
439+
byteArray = Arrays.copyOf(byteArray, maxLength * 2);
440+
}
441+
414442
char extraBytes = (char) (byteArray.length % nBytes);
415443
char[] result = new char[byteArray.length / nBytes + (extraBytes > 0 ? 1 : 0)];
416444
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)