From c464efd8166695bc77e248c97c2dca12a00af327 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Mon, 24 Nov 2025 11:14:13 +0100 Subject: [PATCH] feat: add char and Character mutator --- docs/mutation-framework.md | 4 +-- .../jazzer/mutation/annotation/InRange.java | 2 ++ .../mutator/lang/IntegralMutatorFactory.java | 35 ++++++++++++++++++- .../jazzer/mutation/mutator/StressTest.java | 16 +++++++++ tests/BUILD.bazel | 10 ++++++ .../src/test/java/com/example/CharFuzzer.java | 34 ++++++++++++++++++ 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 tests/src/test/java/com/example/CharFuzzer.java diff --git a/docs/mutation-framework.md b/docs/mutation-framework.md index ea890192f..59c6e4c58 100644 --- a/docs/mutation-framework.md +++ b/docs/mutation-framework.md @@ -56,7 +56,7 @@ Currently supported types are: | Mutator | Type(s) | Notes | |--------------------------------|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Boolean | `boolean`, `Boolean` | | -| Integral | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | | +| Integral | `byte`, `Byte`, `char`, `Character`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | | | Floating point | `float`, `Float`, `double`, `Double` | | | String | `java.lang.String` | | | Enum | `java.lang.Enum` | | @@ -90,7 +90,7 @@ package. | Annotation | Applies To | Notes | |-------------------|----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| | `@Ascii` | `java.lang.String` | `String` should only contain ASCII characters | -| `@InRange` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | Specifies `min` and `max` values of generated integrals | +| `@InRange` | `byte`, `Byte`, `char`, `Character`, `short`, `Short`, `int`, `Integer`, `long`, `Long` | Specifies `min` and `max` values of generated integrals | | `@FloatInRange` | `float`, `Float` | Specifies `min` and `max` values of generated floats | | `@DoubleInRange` | `double`, `Double` | Specifies `min` and `max` values of generated doubles | | `@Positive` | `byte`, `Byte`, `short`, `Short`, `int`, `Integer`, `long`, `Long`, `float`, `Float`, `double`, `Double` | Specifies that only positive values are generated | diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java index 73924bd69..d45e10f8d 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/InRange.java @@ -38,6 +38,8 @@ Byte.class, short.class, Short.class, + char.class, + Character.class, int.class, Integer.class, long.class, diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java index 69c3221dc..17671ef03 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/IntegralMutatorFactory.java @@ -112,6 +112,39 @@ public void write(Short value, DataOutputStream out) throws IOException { out.writeShort(value); } }); + } else if (clazz == char.class || clazz == Character.class) { + return Optional.of( + new AbstractIntegralMutator(type, Character.MIN_VALUE, Character.MAX_VALUE) { + @Override + protected long mutateWithLibFuzzer(long value) { + return LibFuzzerMutate.mutateDefault((char) value, this, 0); + } + + @Override + public Character init(PseudoRandom prng) { + return (char) initImpl(prng); + } + + @Override + public Character mutate(Character value, PseudoRandom prng) { + return (char) mutateImpl(value, prng); + } + + @Override + public Character crossOver(Character value, Character otherValue, PseudoRandom prng) { + return (char) crossOverImpl(value, otherValue, prng); + } + + @Override + public Character read(DataInputStream in) throws IOException { + return (char) forceInRange(in.readChar()); + } + + @Override + public void write(Character value, DataOutputStream out) throws IOException { + out.writeChar(value); + } + }); } else if (clazz == int.class || clazz == Integer.class) { return Optional.of( new AbstractIntegralMutator(type, Integer.MIN_VALUE, Integer.MAX_VALUE) { @@ -189,7 +222,7 @@ public void write(Long value, DataOutputStream out) throws IOException { // Copyright 2022 Google LLC // // Visible for testing. - abstract static class AbstractIntegralMutator extends SerializingMutator { + abstract static class AbstractIntegralMutator extends SerializingMutator { private static final long RANDOM_WALK_RANGE = 5; private final long minValue; private final long maxValue; diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index cbbc018d7..0f5153cb9 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java @@ -775,6 +775,22 @@ void singleParam(short parameter) {} // only passes with ~90% of the optimal parameters. expectedNumberOfDistinctElements( 1 << Short.SIZE, NUM_INITS * NUM_MUTATE_PER_INIT * 9 / 10)), + arguments( + new ParameterHolder() { + void singleParam(char parameter) {} + }.annotatedType(), + "Character", + true, + // init is heavily biased towards special values and only returns a uniformly random + // value in 1 out of 5 calls. + all( + expectedNumberOfDistinctElements(1 << Character.SIZE, boundHits(NUM_INITS, 0.2)), + contains((char) 1, Character.MIN_VALUE, Character.MAX_VALUE)), + // The integral type mutator does not always return uniformly random values and the + // random walk it uses is more likely to produce non-distinct elements, hence the test + // only passes with ~90% of the optimal parameters. + expectedNumberOfDistinctElements( + 1 << Character.SIZE, NUM_INITS * NUM_MUTATE_PER_INIT * 9 / 10)), arguments( new ParameterHolder() { void singleParam(int parameter) {} diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 1290e3e3d..fbc5c647e 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -20,6 +20,16 @@ java_fuzz_target_test( verify_crash_input = False, ) +java_fuzz_target_test( + name = "CharFuzzer", + srcs = [ + "src/test/java/com/example/CharFuzzer.java", + ], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + target_class = "com.example.CharFuzzer", + verify_crash_reproducer = False, +) + java_fuzz_target_test( name = "JpegImageParserAutofuzz", allowed_findings = ["java.lang.NegativeArraySizeException"], diff --git a/tests/src/test/java/com/example/CharFuzzer.java b/tests/src/test/java/com/example/CharFuzzer.java new file mode 100644 index 000000000..91884c379 --- /dev/null +++ b/tests/src/test/java/com/example/CharFuzzer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import com.code_intelligence.jazzer.mutation.annotation.InRange; + +public class CharFuzzer { + private static final char min = '中' - 10; + private static final char max = '中' + 10; + + public static void fuzzerTestOneInput(@InRange(min = min, max = max) char data) { + if (data < min || data > max) { + throw new RuntimeException("Char out of range: " + (int) data); + } + if (data == '中') { + throw new FuzzerSecurityIssueLow("Found the 'secret' char!"); + } + } +}