From 488464864c71cf33e8b02dd991046fa764f59d85 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Fri, 10 Oct 2025 23:49:39 +0200 Subject: [PATCH 01/11] feat: allow creation of parameterized and generic array types In addition to primitive arrays, these types are now also supported: - List [] - List [][][] --- .../collection/ArrayMutatorFactory.java | 18 +++++++++++------ .../jazzer/mutation/support/TypeSupport.java | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java index 29b27eb8f..e822e23b1 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorFactory.java @@ -19,6 +19,7 @@ import static com.code_intelligence.jazzer.mutation.mutator.collection.ChunkMutations.MutationAction.pickRandomMutationAction; import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; import static com.code_intelligence.jazzer.mutation.support.PropertyConstraintSupport.propagatePropertyConstraints; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.extractRawClass; import static java.lang.Math.min; import static java.lang.String.format; @@ -35,6 +36,7 @@ import java.lang.reflect.AnnotatedArrayType; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Array; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Optional; import java.util.function.Predicate; @@ -53,12 +55,16 @@ public Optional> tryCreate( AnnotatedType elementType = ((AnnotatedArrayType) type).getAnnotatedGenericComponentType(); AnnotatedType propagatedElementType = propagatePropertyConstraints(type, elementType); - Class propagatedElementClazz = (Class) propagatedElementType.getType(); - return Optional.of(propagatedElementType) - .flatMap(factory::tryCreate) - .map( - elementMutator -> - new ArrayMutator<>(elementMutator, propagatedElementClazz, minLength, maxLength)); + Type rawType = propagatedElementType.getType(); + return extractRawClass(rawType) + .flatMap( + propagatedElementClass -> + Optional.of(propagatedElementType) + .flatMap(factory::tryCreate) + .map( + elementMutator -> + new ArrayMutator<>( + elementMutator, propagatedElementClass, minLength, maxLength))); } enum CrossOverAction { diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java index 2480d4e93..97730da3e 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java @@ -37,9 +37,12 @@ import java.lang.reflect.AnnotatedTypeVariable; import java.lang.reflect.AnnotatedWildcardType; import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; @@ -687,4 +690,21 @@ public static boolean annotatedTypeEquals(AnnotatedType left, AnnotatedType righ return left.getType().equals(right.getType()) && Arrays.equals(left.getAnnotations(), right.getAnnotations()); } + + public static Optional> extractRawClass(Type type) { + if (type instanceof Class) { + return Optional.of((Class) type); + } else if (type instanceof ParameterizedType) { + return Optional.of((Class) ((ParameterizedType) type).getRawType()); + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + Optional> componentClass = extractRawClass(componentType); + return componentClass.map(aClass -> Array.newInstance(aClass, 0).getClass()); + } else if (type instanceof TypeVariable || type instanceof WildcardType) { + // Default fallback — assume Object array + return Optional.of(Object.class); + } else { + return Optional.empty(); + } + } } From 986ede114ef41441bc56035d701afe92e7b781ad Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Thu, 16 Oct 2025 00:29:46 +0200 Subject: [PATCH 02/11] chore: add array of lists to stress test --- .../jazzer/mutation/mutator/StressTest.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 091879654..b0d832112 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 @@ -579,6 +579,12 @@ null, emptyList(), singletonList(null), singletonList(false), singletonList(true false, distinctElementsRatio(0.30), distinctElementsRatio(0.30)), + arguments( + new TypeHolder<@NotNull List<@NotNull Integer> @NotNull []>() {}.annotatedType(), + "List[]", + false, + manyDistinctElements(), + distinctElementsRatio(0.66)), arguments( new TypeHolder<@NotNull TestEnumThree @NotNull []>() {}.annotatedType(), "Enum[]", From ac12f411c78ba6ed530c0a9282f7befc131d96f9 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Mon, 27 Oct 2025 16:53:10 +0100 Subject: [PATCH 03/11] feat: add meta-annotation for multi-level annotation propagation Allow annotations to be inherited at multiple levels of the type hierarchy, enabling both broad and specific configuration of mutators. Use case: Configure mutators that share common types. For example, annotate a fuzz test method to apply default settings to all String mutators, while still allowing individual String parameters to override those settings with different values. Without this feature, an annotation could only appear once in the inheritance chain, preventing this layered configuration approach. --- .../jazzer/mutation/support/TypeSupport.java | 4 ++ .../utils/IgnoreRecursiveConflicts.java | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java index 97730da3e..682a5a1d5 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java @@ -27,6 +27,7 @@ import com.code_intelligence.jazzer.mutation.annotation.NotNull; import com.code_intelligence.jazzer.mutation.annotation.WithLength; +import com.code_intelligence.jazzer.mutation.utils.IgnoreRecursiveConflicts; import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; @@ -578,6 +579,9 @@ private static Annotation[] checkExtraAnnotations( .collect(Collectors.toCollection(HashSet::new)); for (Annotation annotation : extraAnnotations) { boolean added = existingAnnotationTypes.add(annotation.annotationType()); + if (annotation.annotationType().isAnnotationPresent(IgnoreRecursiveConflicts.class)) { + continue; + } require(added, annotation + " already directly present on " + base); } return extraAnnotations; diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java b/src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java new file mode 100644 index 000000000..b1f194091 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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.code_intelligence.jazzer.mutation.utils; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A meta-annotation to turn off the check in {@code checkExtraAnnotations} that throws if some + * annotation is present multiple times on a type. This allows annotations like e.g. + * {@code @DictionaryProvider} to be propagated down the type hierarchy and accumulated along the + * way. + * + *

E.g. {@code @A("data1") List<@A("data2") String> arg} - the String mutator will can make use + * of {@code @A("data1")} and {@code @A("data2")}, but the List mutator can only see + * {@code @A("data1")}. + */ +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +@Documented +public @interface IgnoreRecursiveConflicts {} From 66cc9b4a9395e1ba3afcccd20930e413b180ddb3 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Fri, 10 Oct 2025 17:18:39 +0200 Subject: [PATCH 04/11] feat internal: weighted random sampler util Enables easy tweaking of probabilities for indidual mutation functions in the future. --- .../mutation/combinator/SamplingUtils.java | 127 ++++++++++++++++++ .../jazzer/mutation/combinator/BUILD.bazel | 1 + .../combinator/SamplingUtilsTest.java | 85 ++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtils.java create mode 100644 src/test/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtilsTest.java diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtils.java b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtils.java new file mode 100644 index 000000000..f96379a16 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright 2025 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.code_intelligence.jazzer.mutation.combinator; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static java.util.Objects.requireNonNull; + +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +public final class SamplingUtils { + + public static Function weightedSampler(T[] values, double[] weights) { + // Use Vose's alias method for O(1) sampling after O(n) preprocessing. + requireNonNull(values, "Values must not be null"); + requireNonNull(weights, "Weights must not be null"); + require(values.length > 0, "Values must not be empty"); + require(values.length == weights.length, "Values and weights must have the same length"); + + double sum = Arrays.stream(weights).sum(); + require(sum > 0, "At least one weight must be positive"); + + int n = values.length; + int[] alias = new int[n]; + double[] probability = new double[n]; + double[] scaledWeights = Arrays.stream(weights).map(w -> w * n / sum).toArray(); + int[] small = new int[n]; + int[] large = new int[n]; + int smallCount = 0; + int largeCount = 0; + for (int i = 0; i < n; i++) { + if (scaledWeights[i] < 1.0) { + small[smallCount++] = i; + } else { + large[largeCount++] = i; + } + } + + while (smallCount > 0 && largeCount > 0) { + int less = small[--smallCount]; + int more = large[--largeCount]; + + probability[less] = scaledWeights[less]; + alias[less] = more; + scaledWeights[more] = (scaledWeights[more] + scaledWeights[less]) - 1.0; + + if (scaledWeights[more] < 1.0) { + small[smallCount++] = more; + } else { + large[largeCount++] = more; + } + } + while (largeCount > 0) { + probability[large[--largeCount]] = 1.0; + } + + while (smallCount > 0) { + probability[small[--smallCount]] = 1.0; + } + return (PseudoRandom random) -> { + int column = random.indexIn(n); + return values[random.closedRange(0.0, 1.0) < probability[column] ? column : alias[column]]; + }; + } + + public static Function weightedSampler( + List> weightedFunctions) { + requireNonNull(weightedFunctions, "Weighted functions must not be null"); + require(!weightedFunctions.isEmpty(), "Weighted functions must not be empty"); + + double[] weights = weightedFunctions.stream().mapToDouble(m -> m.weight).toArray(); + + T[] fns = (T[]) weightedFunctions.stream().map(m -> m.value).toArray(Object[]::new); + + return weightedSampler(fns, weights); + } + + @SafeVarargs + public static Function weightedSampler( + Optional>... values) { + return weightedSampler( + Arrays.stream(values) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList())); + } + + /** + * A simple struct to hold a value and its weight. It is here just for stylistic reasons, to make + * the definitions of weights and values more readable. + */ + public static class WeightedValue { + public final double weight; + public final T value; + + public WeightedValue(double weight, T value) { + this.value = value; + this.weight = weight; + } + + public static WeightedValue of(double weight, T fn) { + return new WeightedValue<>(weight, fn); + } + + public static Optional> ofOptional(double weight, T fn) { + return Optional.of(new WeightedValue<>(weight, fn)); + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel index 033c03b64..75d163899 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/mutation/combinator/BUILD.bazel @@ -8,6 +8,7 @@ java_test_suite( deps = [ "//src/main/java/com/code_intelligence/jazzer/mutation/api", "//src/main/java/com/code_intelligence/jazzer/mutation/combinator", + "//src/main/java/com/code_intelligence/jazzer/mutation/engine", "//src/main/java/com/code_intelligence/jazzer/mutation/support", "//src/test/java/com/code_intelligence/jazzer/mutation/support:test_support", ], diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtilsTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtilsTest.java new file mode 100644 index 000000000..84549c002 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/combinator/SamplingUtilsTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025 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.code_intelligence.jazzer.mutation.combinator; + +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; +import com.code_intelligence.jazzer.mutation.engine.SeededPseudoRandom; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class SamplingUtilsTest { + static Stream weightsProvider() { + final int N = 1000000; + final double T = 0.03; + return Stream.of( + arguments(N, T, new double[] {1.0, 1.0, 1.0}), + arguments(N, T, new double[] {1.0, 2.0, 3.0, 4.0, 5.0}), + arguments(N, T, new double[] {0.1, 0.2, 0.3, 0.4}), + arguments(N, T, new double[] {10.0, 0.0, 0.1, 0.0, 90.0}), + arguments(N, T, new double[] {5.0, 5.0, 0.0, 0.0, 0.01, 5.0, 5.0}), + arguments(N, T, new double[] {0.0, 0.0, 0.0, 1.0}), + arguments(N, T, new double[] {1.0}), + arguments(N, T, new double[] {0.01, 0.01, 0.01, 0.97}), + arguments(N, T, new double[] {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}), + arguments(N, T, new double[] {0.001, 0.002, 0.003, 0.004, 0.005}), + arguments(N, T, new double[] {0.001, 0.002, 0.003, 0.004, 0.000001, 10.0}), + arguments(N, T, new double[] {0.001, 1000.0, 0.003, 10000.0, 0.005}), + arguments(N, T, IntStream.range(1, 10).mapToDouble(i -> i).toArray()), + arguments(N, 0.09, IntStream.range(1, 100).mapToDouble(i -> 1.0).toArray()), + arguments(N, 0.15, IntStream.range(1, 1000).mapToDouble(i -> 1.0).toArray()), + arguments(10000000, 0.15, IntStream.range(1, 10000).mapToDouble(i -> 1.0).toArray()), + arguments(100000000, 0.16, IntStream.range(1, 100000).mapToDouble(i -> 1.0).toArray())); + } + + @ParameterizedTest + @MethodSource("weightsProvider") + public void testWeightedSampler(int trials, double tolerance, double[] weights) { + Integer[] indices = IntStream.range(0, weights.length).boxed().toArray(Integer[]::new); + Function sampler = SamplingUtils.weightedSampler(indices, weights); + + PseudoRandom random = new SeededPseudoRandom(12345); + int[] counts = new int[indices.length]; + for (int i = 0; i < trials; i++) { + counts[sampler.apply(random)]++; + } + + // Calculate expected probabilities that are proportional to the weights. + double[] pExpected = new double[weights.length]; + double sum = 0.0; + for (double w : weights) { + sum += w; + } + for (int i = 0; i < weights.length; i++) { + pExpected[i] = weights[i] / sum; + } + + double tol = (double) trials / weights.length * tolerance; // 5% of expected count + // Ensure that the frequencies are within 5% of the expected frequencies. + for (int i = 0; i < weights.length; i++) { + double expectedCount = trials * pExpected[i]; + assert Math.abs(counts[i] - expectedCount) < tol + : String.format( + "Count for index %d out of tolerance: got %d, expected ~%.2f", + i, counts[i], expectedCount); + } + } +} From 770e8f8250b9bba30b89d6f5a0556c571857f819 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Thu, 30 Oct 2025 13:10:07 +0100 Subject: [PATCH 05/11] chore: add global MutationRuntime For now it only stores the fuzz test method --- .../jazzer/mutation/ArgumentsMutator.java | 3 +++ .../jazzer/mutation/BUILD.bazel | 1 + .../jazzer/mutation/runtime/BUILD.bazel | 11 ++++++++ .../mutation/runtime/MutationRuntime.java | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/runtime/BUILD.bazel create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/runtime/MutationRuntime.java diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java index 86bfdd67c..8c758d5a1 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java @@ -30,6 +30,7 @@ import com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators; import com.code_intelligence.jazzer.mutation.engine.SeededPseudoRandom; import com.code_intelligence.jazzer.mutation.mutator.Mutators; +import com.code_intelligence.jazzer.mutation.runtime.MutationRuntime; import com.code_intelligence.jazzer.mutation.support.Preconditions; import com.code_intelligence.jazzer.utils.Log; import java.io.ByteArrayInputStream; @@ -97,6 +98,8 @@ public static Optional forMethod( Log.error(validationError.getMessage()); throw validationError; } + MutationRuntime.fuzzTestMethod = method; + DictionaryProvider[] typeDictionaries = method.getAnnotationsByType(DictionaryProvider.class); return toArrayOrEmpty( stream(method.getAnnotatedParameterTypes()) .map( diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel index 73e0472a6..3bb80eb86 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel @@ -11,6 +11,7 @@ java_library( "//src/main/java/com/code_intelligence/jazzer/mutation/combinator", "//src/main/java/com/code_intelligence/jazzer/mutation/engine", "//src/main/java/com/code_intelligence/jazzer/mutation/mutator", + "//src/main/java/com/code_intelligence/jazzer/mutation/runtime", "//src/main/java/com/code_intelligence/jazzer/mutation/support", "//src/main/java/com/code_intelligence/jazzer/utils:log", ], diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/runtime/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/runtime/BUILD.bazel new file mode 100644 index 000000000..4cc62d3e5 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/runtime/BUILD.bazel @@ -0,0 +1,11 @@ +java_library( + name = "runtime", + srcs = glob(["*.java"]), + visibility = [ + "//selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/mutator/lang:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/mutation:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/mutation:__subpackages__", + "//src/test/java/com/code_intelligence/jazzer/mutation:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation:__subpackages__", + ], +) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/runtime/MutationRuntime.java b/src/main/java/com/code_intelligence/jazzer/mutation/runtime/MutationRuntime.java new file mode 100644 index 000000000..7fe1205cc --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/runtime/MutationRuntime.java @@ -0,0 +1,25 @@ +/* + * 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.code_intelligence.jazzer.mutation.runtime; + +import java.lang.reflect.Method; + +/** Runtime information to be used by mutators. */ +public class MutationRuntime { + /** The fuzz test method currently being executed. */ + public static Method fuzzTestMethod; +} From bc4a764b36824c7fd204526a886dea122ed58c2d Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Thu, 16 Oct 2025 17:56:42 +0200 Subject: [PATCH 06/11] feat: @DictionaryProvider - propagate values to type mutators This is just the enabling work. Methods and types annotated by @DictionaryProvider recursively propagate this annotation down the type hierarchy by default (can set to be for the annotated type only). Any mutator can now be adapted to use the user-provided values this annotation points to. --- .../jazzer/junit/BUILD.bazel | 1 + .../jazzer/mutation/ArgumentsMutator.java | 10 +- .../jazzer/mutation/BUILD.bazel | 1 + .../annotation/DictionaryProvider.java | 95 +++++++++ .../support/DictionaryProviderSupport.java | 108 +++++++++++ .../jazzer/mutation/support/BUILD.bazel | 2 + .../DictionaryProviderSupportTest.java | 182 ++++++++++++++++++ 7 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/annotation/DictionaryProvider.java create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupport.java create mode 100644 src/test/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupportTest.java diff --git a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel index 8338cb5c6..9e5074f49 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel @@ -38,6 +38,7 @@ java_library( "//examples/junit/src/test/java/com/example:__pkg__", "//selffuzz/src/test/java/com/code_intelligence/selffuzz:__subpackages__", "//src/test/java/com/code_intelligence/jazzer/junit:__pkg__", + "//src/test/java/com/code_intelligence/jazzer/mutation/support:__pkg__", ], exports = [ ":lifecycle", diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java index 8c758d5a1..3a7fb923d 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java @@ -23,6 +23,7 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.joining; +import com.code_intelligence.jazzer.mutation.annotation.DictionaryProvider; import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; import com.code_intelligence.jazzer.mutation.api.PseudoRandom; import com.code_intelligence.jazzer.mutation.api.SerializingMutator; @@ -32,6 +33,7 @@ import com.code_intelligence.jazzer.mutation.mutator.Mutators; import com.code_intelligence.jazzer.mutation.runtime.MutationRuntime; import com.code_intelligence.jazzer.mutation.support.Preconditions; +import com.code_intelligence.jazzer.mutation.support.TypeSupport; import com.code_intelligence.jazzer.utils.Log; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -104,7 +106,13 @@ public static Optional forMethod( stream(method.getAnnotatedParameterTypes()) .map( type -> { - Optional> mutator = mutatorFactory.tryCreate(type); + // Forward all DictionaryProvider annotations of the fuzz test method to each + // arg. + AnnotatedType t = type; + for (DictionaryProvider dict : typeDictionaries) { + t = TypeSupport.withExtraAnnotations(t, dict); + } + Optional> mutator = mutatorFactory.tryCreate(t); if (!mutator.isPresent()) { Log.error( String.format( diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel index 3bb80eb86..2cbedab19 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel @@ -13,6 +13,7 @@ java_library( "//src/main/java/com/code_intelligence/jazzer/mutation/mutator", "//src/main/java/com/code_intelligence/jazzer/mutation/runtime", "//src/main/java/com/code_intelligence/jazzer/mutation/support", + "//src/main/java/com/code_intelligence/jazzer/mutation/utils", "//src/main/java/com/code_intelligence/jazzer/utils:log", ], ) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DictionaryProvider.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DictionaryProvider.java new file mode 100644 index 000000000..a14a11482 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/DictionaryProvider.java @@ -0,0 +1,95 @@ +/* + * 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.code_intelligence.jazzer.mutation.annotation; + +import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.RECURSIVE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.code_intelligence.jazzer.mutation.utils.IgnoreRecursiveConflicts; +import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Provides dictionary values to user-selected mutator types. Currently supported mutators are: + * + *

    + *
  • String mutator + *
  • Integral mutators (byte, short, int, long) + *
+ * + *

This annotation can be applied to fuzz test methods and any parameter type or subtype. By + * default, this annotation is propagated to all nested subtypes unless specified otherwise via the + * {@link #constraint()} attribute. + * + *

Example usage: + * + *

{@code
+ * public class MyFuzzTarget {
+ *
+ *   static Stream dictionaryVisibleByAllArgumentMutators() {
+ *     return Stream.of("example1", "example2", "example3", 1232187321, -182371);
+ *   }
+ *
+ *   static Stream dictionaryVisibleOnlyByAnotherInput() {
+ *     return Stream.of("code-intelligence.com", "secret.url.1082h3u21ibsdsazuvbsa.com");
+ *   }
+ *
+ *   @DictionaryProvider("dictionaryVisibleByAllArgumentMutators")
+ *   @FuzzTest
+ *   public void fuzzerTestOneInput(String input, @DictionaryProvider("dictionaryVisibleOnlyByAnotherInput") String anotherInput) {
+ *     // Fuzzing logic here
+ *   }
+ * }
+ * }
+ * + * In this example, the mutator for the String parameter {@code input} of the fuzz test method + * {@code fuzzerTestOneInput} will be using the values returned by {@code provide} method during + * mutation, while the mutator for String {@code anotherInput} will use values from both methods: + * from the method-level {@code DictionaryProvider} annotation that uses {@code provide} and the + * parameter-level {@code DictionaryProvider} annotation that uses {@code provideSomethingElse}. + */ +@Target({ElementType.METHOD, TYPE_USE}) +@Retention(RUNTIME) +@IgnoreRecursiveConflicts +@PropertyConstraint +public @interface DictionaryProvider { + /** + * Specifies supplier methods that generate dictionary values for fuzzing the annotated method or + * type. The specified supplier methods must be static and return a {@code Stream } of values. + * The values don't need to match the type of the annotated method or parameter exactly. The + * mutation framework will extract only the values that are compatible with the target type. + */ + String[] value() default {""}; + + /** + * This {@code DictionaryProvider} will be used with probability {@code 1/p} by the mutator + * responsible for fitting types. Not all mutators respect this probability. + */ + int pInv() default 10; + + /** + * Defines the scope of the annotation. Possible values are defined in {@link + * com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. It is convenient to use {@code + * RECURSIVE} as the default value here, as dictionary objects are typically used for complex + * types (e.g. custom classes) where the annotation is placed directly on the method or parameter + * and is expected to apply to all nested fields. + */ + String constraint() default RECURSIVE; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupport.java new file mode 100644 index 000000000..5be8e6b9c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupport.java @@ -0,0 +1,108 @@ +/* + * Copyright 2025 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.code_intelligence.jazzer.mutation.support; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; + +import com.code_intelligence.jazzer.mutation.annotation.DictionaryProvider; +import com.code_intelligence.jazzer.mutation.runtime.MutationRuntime; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class DictionaryProviderSupport { + + /** + * Extract inverse probability of the very first {@code DictionaryProvider} annotation on the + * given type. The {@code @DictionaryProvider} annotation directly on the type is preferred; if + * there is none, the first one appended because of {@code PropertyConstraint.RECURSIVE} is used. + * Any further {@code @DictionaryProvider} annotations appended later to this type because of + * {@code PropertyConstraint.RECURSIVE}, are ignored. Callers should ensure that at least one + * {@code @DictionaryProvider} annotation is present on the type. + */ + public static int extractFirstInvProbability(AnnotatedType type) { + // If there are several @DictionaryProvider annotations on the type, this will take the most + // immediate one, because @DictionaryProvider is not repeatable. + DictionaryProvider[] dictObj = type.getAnnotationsByType(DictionaryProvider.class); + if (dictObj.length == 0) { + // If we are here, it's a bug in the caller. + throw new IllegalStateException("Expected to find @DictionaryProvider, but found none."); + } + int pInv = dictObj[0].pInv(); + require(pInv >= 2, "@DictionaryProvider.pInv must be at least 2"); + return pInv; + } + + /** Extract the provider streams using MutatorRuntime */ + public static Optional> extractRawValues(AnnotatedType type) { + DictionaryProvider[] providers = + Arrays.stream(type.getAnnotations()) + .filter(a -> a instanceof DictionaryProvider) + .map(a -> (DictionaryProvider) a) + .toArray(DictionaryProvider[]::new); + if (providers.length == 0) { + return Optional.empty(); + } + HashSet providerMethodNames = + Arrays.stream(providers) + .map(DictionaryProvider::value) + .flatMap(Arrays::stream) + .filter(name -> !name.isEmpty()) + .collect(Collectors.toCollection(HashSet::new)); + if (providerMethodNames.isEmpty()) { + return Optional.empty(); + } + Map fuzzTestMethods = + Arrays.stream(MutationRuntime.fuzzTestMethod.getDeclaringClass().getDeclaredMethods()) + .filter(m -> m.getParameterCount() == 0) + .filter(m -> m.getReturnType().equals(Stream.class)) + .filter( + m -> + (m.getModifiers() & java.lang.reflect.Modifier.STATIC) + == java.lang.reflect.Modifier.STATIC) + .collect(Collectors.toMap(Method::getName, m -> m)); + return Optional.ofNullable( + providerMethodNames.stream() + .flatMap( + name -> { + Method m = fuzzTestMethods.get(name); + if (m == null) { + throw new IllegalStateException( + "Found no static supplier method 'Stream " + + name + + "()' in class " + + MutationRuntime.fuzzTestMethod.getDeclaringClass().getName()); + } + try { + m.setAccessible(true); + return (Stream) m.invoke(null); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot access method " + name, e); + } catch (InvocationTargetException e) { + throw new RuntimeException( + "Supplier method " + name + " threw an exception", e.getCause()); + } + }) + .distinct()); + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel index a87f8a391..63e19e5c6 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel @@ -29,6 +29,8 @@ java_test_suite( runner = "junit5", deps = [ ":test_support", + "//deploy:jazzer-project", + "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test", "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", "//src/main/java/com/code_intelligence/jazzer/mutation/support", "//src/main/java/com/code_intelligence/jazzer/mutation/utils", diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupportTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupportTest.java new file mode 100644 index 000000000..9a9574218 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/DictionaryProviderSupportTest.java @@ -0,0 +1,182 @@ +/* + * 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.code_intelligence.jazzer.mutation.support; + +import static com.google.common.truth.Truth.assertThat; + +import com.code_intelligence.jazzer.mutation.annotation.DictionaryProvider; +import com.code_intelligence.jazzer.mutation.runtime.MutationRuntime; +import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +class DictionaryProviderSupportTest { + + /* Dummy fuzz test method to add to MutatorRuntime. */ + public void dummyFuzzTestMethod() {} + + static { + try { + MutationRuntime.fuzzTestMethod = + DictionaryProviderSupportTest.class.getMethod("dummyFuzzTestMethod"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + public static Stream myProvider() { + return Stream.of("value1", "value2", "value3"); + } + + public static Stream myProvider2() { + return Stream.of("value1", "value2", "value3", "value4"); + } + + @Test + void testExtractFirstInvProbability_Default() { + AnnotatedType type = + new TypeHolder<@DictionaryProvider("myProvider") String>() {}.annotatedType(); + int pInv = DictionaryProviderSupport.extractFirstInvProbability(type); + assertThat(pInv).isEqualTo(10); + } + + @Test + void testExtractFirstInvProbability_OneUserDefined() { + AnnotatedType type = + new TypeHolder< + @DictionaryProvider(value = "myProvider2", pInv = 2) String>() {}.annotatedType(); + int pInv = DictionaryProviderSupport.extractFirstInvProbability(type); + assertThat(pInv).isEqualTo(2); + } + + @Test + void testExtractFirstInvProbability_TwoWithLastUsed() { + AnnotatedType type = + TypeSupport.withExtraAnnotations( + new TypeHolder< + @DictionaryProvider(value = "myProvider", pInv = 2) String>() {}.annotatedType(), + withDictionaryProviderImplementation(new String[] {"myProvider2"}, 3)); + int pInv = DictionaryProviderSupport.extractFirstInvProbability(type); + assertThat(pInv).isEqualTo(2); + } + + @Test + void testExtractRawValues_OneAnnotation() { + AnnotatedType type = + new TypeHolder<@DictionaryProvider("myProvider") String>() {}.annotatedType(); + Optional> elements = DictionaryProviderSupport.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3"); + } + + @Test + void testExtractProviderStreams_JoinStreamsInOneProvider() { + AnnotatedType type = + new TypeHolder< + @DictionaryProvider({"myProvider", "myProvider2"}) String>() {}.annotatedType(); + Optional> elements = DictionaryProviderSupport.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + } + + @Test + void testExtractRawValues_JoinTwoFromOne() { + AnnotatedType type = + new TypeHolder< + @DictionaryProvider({"myProvider", "myProvider2"}) String>() {}.annotatedType(); + Optional> elements = DictionaryProviderSupport.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + } + + @Test + void testExtractRawValues_JoinFromTwoSeparateAnnotations() { + AnnotatedType type = + TypeSupport.withExtraAnnotations( + new TypeHolder<@DictionaryProvider("myProvider2") String>() {}.annotatedType(), + withDictionaryProviderImplementation(new String[] {"myProvider"}, 5)); + Optional> elements = DictionaryProviderSupport.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + } + + private static DictionaryProvider withDictionaryProviderImplementation(String[] value, int pInv) { + return withDictionaryProviderImplementation(value, pInv, PropertyConstraint.RECURSIVE); + } + + private static DictionaryProvider withDictionaryProviderImplementation( + String[] value, int pInv, String constraint) { + return new DictionaryProvider() { + @Override + public String[] value() { + return value; + } + + @Override + public int pInv() { + return pInv; + } + + @Override + public String constraint() { + return constraint; + } + + @Override + public Class annotationType() { + return DictionaryProvider.class; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DictionaryProvider)) { + return false; + } + DictionaryProvider other = (DictionaryProvider) o; + return Arrays.equals(this.value(), other.value()) + && this.pInv() == other.pInv() + && this.constraint().equals(other.constraint()); + } + + @Override + public int hashCode() { + int hash = 0; + hash += Arrays.hashCode(value()) * 127; + hash += pInv() * 31 * 127; + hash += constraint().hashCode() * 127; + return hash; + } + + @Override + public String toString() { + return "@" + + DictionaryProvider.class.getName() + + "(value={" + + String.join(", ", value()) + + "}, pInv=" + + pInv() + + ", constraint=" + + constraint() + + ")"; + } + }; + } +} From 60a488b868fe0d94bcf50438fd56ebde23e0c312 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Fri, 17 Oct 2025 00:58:19 +0200 Subject: [PATCH 07/11] feat: String mutator now uses @DictionaryProvider The StringMutatorFactory now extracts applicable Strings from the @DictionaryProvider and uses them during mutation according to the pInv of the last @DictionaryProvider annotation it found on this type. --- .../mutator/lang/StringMutatorFactory.java | 109 +++++++++++++- .../jazzer/mutation/support/BUILD.bazel | 1 + tests/BUILD.bazel | 22 +++ .../DictionaryProviderFuzzerLongString.java | 138 ++++++++++++++++++ 4 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 tests/src/test/java/com/example/DictionaryProviderFuzzerLongString.java diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java index 32f8852c3..5f502dad9 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/StringMutatorFactory.java @@ -17,7 +17,10 @@ package com.code_intelligence.jazzer.mutation.mutator.lang; import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; -import static com.code_intelligence.jazzer.mutation.support.TypeSupport.*; +import static com.code_intelligence.jazzer.mutation.support.DictionaryProviderSupport.extractFirstInvProbability; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.findFirstParentIfClass; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.notNull; +import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withLength; import com.code_intelligence.jazzer.mutation.annotation.Ascii; import com.code_intelligence.jazzer.mutation.annotation.UrlSegment; @@ -25,13 +28,19 @@ import com.code_intelligence.jazzer.mutation.api.Debuggable; import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.PseudoRandom; import com.code_intelligence.jazzer.mutation.api.SerializingMutator; import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutatorFactory; +import com.code_intelligence.jazzer.mutation.support.DictionaryProviderSupport; import com.code_intelligence.jazzer.mutation.support.TypeHolder; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; import java.lang.reflect.AnnotatedType; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.function.Predicate; +import java.util.stream.Stream; final class StringMutatorFactory implements MutatorFactory { private static final int HEADER_MASK = 0b1100_0000; @@ -176,7 +185,9 @@ public Optional> tryCreate( AnnotatedType innerByteArray = notNull(withLength(new TypeHolder() {}.annotatedType(), min, max)); - return LibFuzzerMutatorFactory.tryCreate(innerByteArray); + Optional> innerMutator = + LibFuzzerMutatorFactory.tryCreate(innerByteArray); + return UserDictionaryMutatorWrapper.of(innerMutator, type, min, max); }) .map( byteArrayMutator -> { @@ -198,4 +209,98 @@ public Optional> tryCreate( (Predicate inCycle) -> "String"); }); } + + private static final class UserDictionaryMutatorWrapper extends SerializingMutator { + private final byte[][] dictionaryValues; + private final SerializingMutator basicMutator; + private final int pInv; + + public static Optional> of( + Optional> mutator, AnnotatedType type, int minSize, int maxSize) { + if (!mutator.isPresent()) { + return Optional.empty(); + } + Optional values = generateDictionaryValues(type, minSize, maxSize); + if (!values.isPresent()) { + return mutator; + } + return Optional.of( + new UserDictionaryMutatorWrapper( + (SerializingMutator) mutator.get(), + values.get(), + extractFirstInvProbability(type))); + } + + public UserDictionaryMutatorWrapper( + SerializingMutator basicMutator, byte[][] dictionaryValues, int pInv) { + this.basicMutator = basicMutator; + this.dictionaryValues = dictionaryValues; + this.pInv = pInv; + } + + public static Optional generateDictionaryValues( + AnnotatedType type, int minSize, int maxSize) { + return DictionaryProviderSupport.extractRawValues(type) + .map( + stream -> + stream + .flatMap( + o -> { + if (o instanceof String) { + return Stream.of(((String) o).getBytes(StandardCharsets.UTF_8)); + } else { + return Stream.empty(); + } + }) + .filter(b -> b.length >= minSize && b.length <= maxSize) + .distinct() + .toArray(byte[][]::new)); + } + + @Override + public String toDebugString(Predicate isInCycle) { + return "String"; + } + + @Override + public byte[] read(DataInputStream in) throws IOException { + return basicMutator.read(in); + } + + @Override + public void write(byte[] value, DataOutputStream out) throws IOException { + basicMutator.write(value, out); + } + + @Override + public byte[] detach(byte[] value) { + return basicMutator.detach(value); + } + + @Override + public byte[] init(PseudoRandom prng) { + if (prng.trueInOneOutOf(pInv)) { + return prng.pickIn(dictionaryValues); + } + return basicMutator.init(prng); + } + + @Override + public byte[] mutate(byte[] value, PseudoRandom prng) { + if (prng.trueInOneOutOf(pInv)) { + return prng.pickIn(dictionaryValues); + } + return basicMutator.mutate(value, prng); + } + + @Override + public byte[] crossOver(byte[] value, byte[] otherValue, PseudoRandom prng) { + return basicMutator.crossOver(value, otherValue, prng); + } + + @Override + public boolean hasFixedSize() { + return false; + } + } } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel index 30694ec5b..e2c0517eb 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel @@ -7,6 +7,7 @@ java_library( ], deps = [ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + "//src/main/java/com/code_intelligence/jazzer/mutation/runtime", "//src/main/java/com/code_intelligence/jazzer/mutation/utils", "//src/main/java/com/code_intelligence/jazzer/utils:log", ], diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 22dca2a3b..2c8d7402f 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -20,6 +20,28 @@ java_fuzz_target_test( verify_crash_input = False, ) +java_fuzz_target_test( + name = "DictionaryProviderFuzzerLongString", + srcs = [ + "src/test/java/com/example/DictionaryProviderFuzzerLongString.java", + ], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + fuzzer_args = [ + "-runs=10000", + ], + target_class = "com.example.DictionaryProviderFuzzerLongString", + verify_crash_input = False, + verify_crash_reproducer = False, + deps = [ + "//deploy:jazzer-junit", + "//deploy:jazzer-project", + "@maven//:com_google_truth_truth", + "@maven//:org_junit_jupiter_junit_jupiter_api", + "@maven//:org_junit_jupiter_junit_jupiter_engine", + "@maven//:org_junit_platform_junit_platform_launcher", + ], +) + java_fuzz_target_test( name = "JpegImageParserAutofuzz", allowed_findings = ["java.lang.NegativeArraySizeException"], diff --git a/tests/src/test/java/com/example/DictionaryProviderFuzzerLongString.java b/tests/src/test/java/com/example/DictionaryProviderFuzzerLongString.java new file mode 100644 index 000000000..794c66686 --- /dev/null +++ b/tests/src/test/java/com/example/DictionaryProviderFuzzerLongString.java @@ -0,0 +1,138 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import com.code_intelligence.jazzer.junit.FuzzTest; +import com.code_intelligence.jazzer.mutation.annotation.DictionaryProvider; +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.annotation.WithSize; +import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length; +import java.util.List; +import java.util.stream.Stream; + +public class DictionaryProviderFuzzerLongString { + private static final String str00 = repeat("0123456789abcdef", 50); + private static final String str01 = repeat("sitting duck suprime", 53); + private static final String str10 = repeat("poa0189fbhBHOVBO781%", 30); + private static final String unused = repeat("XdeadbeefX", 21); + + public static Stream dict0() { + return Stream.of( + str00, + str01, + // We can mix all kinds of values in the same dictionary. + // Each mutator only takes the values it can use. + 123, + 4567899999L); + } + + public static Stream dict1() { + return Stream.of(str10); + } + + public static Stream emptyDict() { + return Stream.of(); + } + + public static Stream unusedDictionary() { + return Stream.of(unused); + } + + @FuzzTest + // Just propagate the dictionary to all types of the fuzz test method that can use it. + // Annotating individual String parameters is also possible. + @DictionaryProvider( + value = {"dict0"}, + // Here we use a very low probability for picking dictionary values. + // It gets overwritten for some arguments below. + pInv = 1000000000) + public static void fuzzerTestOneInput( + @NotNull + // Extend the maximum length of the String so that the dictionary values can actually be + // used + @WithUtf8Length(max = 10000) + // The String mutator for this argument will use "dict1" and "emptyDict" with pInv = 2 + // for all dictionary entries. + @DictionaryProvider( + value = {"emptyDict"}, + // Set pInv = 2 for the String mutator + pInv = 2) + String data00, + + // Identical annotations as for data00 + @NotNull + @WithUtf8Length(max = 10000) + @DictionaryProvider( + value = {"emptyDict"}, + pInv = 2) + String data01, + + // The String mutator, inside the List mutator for this argument will use "dict0" and + // "dict1" with pInv = 2 for all dictionary entries. + // Note that the String mutator is not directly annotated, and gets annotated because + // @DictionaryProvider has PropertyConstraint.RECURSIVE + @DictionaryProvider( + value = {"dict1"}, + pInv = 2) + @NotNull + @WithSize(max = 2) + List<@NotNull String> data1, + + // The String mutator for this argument will use entries from + // @DictionaryProvider(value={"dict0"}, pInv = 1000000000), that get propagated here from the + // method annotation. + @NotNull String data2) { + + // This should only happen 2:1000000000 times. + assertThat(data2.equals(str00)).isFalse(); + assertThat(data2.equals(str01)).isFalse(); + + // Error: matched a long string from dictionary entry this variable was NOT annotated with. + // This should never happen. + assertThat(data00.equals(str10)).isFalse(); + assertThat(data00.equals(unused)).isFalse(); + assertThat(data01.equals(str10)).isFalse(); + assertThat(data01.equals(unused)).isFalse(); + assertThat(data1.equals(unused)).isFalse(); + assertThat(data2.equals(str10)).isFalse(); + assertThat(data2.equals(unused)).isFalse(); + + /* + * libFuzzer's table of recent compares only allows 64 bytes, so asking the fuzzer to construct + * these long strings would run for a very very long time without finding them. However, with a + * @DictionaryProvider this problem is trivial, because we can directly provide these long strings to + * the fuzzer, and also force that they are used more often by setting pInv to a low value close to 2. + */ + if (data00.equals(str00) + && data01.equals(str01) + && !data1.isEmpty() + && data1.get(0).equals(str10)) { + throw new FuzzerSecurityIssueLow("Found all long strings as expected"); + } + } + + private static String repeat(String str, int count) { + StringBuilder sb = new StringBuilder(str.length() * count); + for (int i = 0; i < count; i++) { + sb.append(str); + } + return sb.toString(); + } +} From 4461462c99fa81085832ed9992584a4e4c663816 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Tue, 28 Oct 2025 20:20:14 +0100 Subject: [PATCH 08/11] feat: Integral mutators now use @DictionaryProvider --- .../mutator/lang/IntegralMutatorFactory.java | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) 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..41fd16336 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 @@ -24,7 +24,10 @@ import com.code_intelligence.jazzer.mutation.api.MutatorFactory; import com.code_intelligence.jazzer.mutation.api.PseudoRandom; import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.combinator.SamplingUtils; +import com.code_intelligence.jazzer.mutation.combinator.SamplingUtils.WeightedValue; import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutate; +import com.code_intelligence.jazzer.mutation.support.DictionaryProviderSupport; import com.code_intelligence.jazzer.mutation.support.RangeSupport; import com.code_intelligence.jazzer.mutation.support.RangeSupport.LongRange; import com.google.errorprone.annotations.ForOverride; @@ -33,7 +36,10 @@ import java.io.IOException; import java.lang.reflect.AnnotatedType; import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.LongStream; @@ -196,6 +202,8 @@ abstract static class AbstractIntegralMutator extends Serializ private final int largestMutableBitNegative; private final int largestMutableBitPositive; private final long[] specialValues; + private final long[] dictionaryValues; + private final Function mutationFunctionSampler; AbstractIntegralMutator( AnnotatedType type, long defaultMinValueForType, long defaultMaxValueForType) { @@ -231,6 +239,50 @@ abstract static class AbstractIntegralMutator extends Serializ largestMutableBitPositive = bitWidth(maxValue); } this.specialValues = collectSpecialValues(minValue, maxValue); + + this.dictionaryValues = + DictionaryProviderSupport.extractRawValues(type) + .map( + stream -> + stream + .filter(v -> v instanceof Number) + .map(v -> ((Number) v).longValue()) + .filter(v -> v >= minValue) + .filter(v -> v <= maxValue) + .sorted() + .mapToLong(Long::longValue) + .toArray()) + .orElse(null); + List> f = new ArrayList<>(); + f.add(new WeightedValue<>(1.0, MutationFunction.BIT_FLIP)); + f.add(new WeightedValue<>(1.0, MutationFunction.RANDOM_WALK)); + f.add(new WeightedValue<>(1.0, MutationFunction.RANDOM_VALUE)); + f.add(new WeightedValue<>(1.0, MutationFunction.LIBFUZZER)); + if (dictionaryValues != null && dictionaryValues.length > 0) { + // Since weights here are relative, we need to adjust the weight of user dictionary mutator + // so that it is taken proportionate the inverse probability specified in the annotation. + // Basically, we need to scale up the weight for pInv: + // 1/p --- x? + // 1- 1/p --- totalFuncWeights + // x = (1/p * totalFuncWeights) / (1 - 1/p) + // = totalFuncWeights / (p - 1) + double totalFuncWeights = 0.0; + for (WeightedValue wf : f) { + totalFuncWeights += wf.weight; + } + int invProbability = DictionaryProviderSupport.extractFirstInvProbability(type); + double perValueWeight = totalFuncWeights / (invProbability - 1); + f.add(new WeightedValue<>(perValueWeight, MutationFunction.DICTIONARY_VALUE)); + } + this.mutationFunctionSampler = SamplingUtils.weightedSampler(f); + } + + private enum MutationFunction { + BIT_FLIP, + DICTIONARY_VALUE, + LIBFUZZER, + RANDOM_VALUE, + RANDOM_WALK, } private static long[] collectSpecialValues(long minValue, long maxValue) { @@ -262,20 +314,25 @@ protected final long mutateImpl(long value, PseudoRandom prng) { final long previousValue = value; // Mutate in a loop to verify that we really mutated. do { - switch (prng.indexIn(4)) { - case 0: + switch (mutationFunctionSampler.apply(prng)) { + case BIT_FLIP: value = bitFlip(value, prng); break; - case 1: + case RANDOM_WALK: value = randomWalk(value, prng); break; - case 2: + case RANDOM_VALUE: value = prng.closedRange(minValue, maxValue); break; - case 3: + case LIBFUZZER: // TODO: Replace this with a structure-aware dictionary/TORC search similar to fuzztest. value = forceInRange(mutateWithLibFuzzer(value)); break; + case DICTIONARY_VALUE: + value = dictionaryValues[prng.indexIn(dictionaryValues.length)]; + break; + default: + throw new AssertionError("Invalid mutation function."); } } while (value == previousValue); return value; From 84240e57053f0eacaf40b4a3288c2b7966eac24a Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 29 Oct 2025 09:43:13 +0100 Subject: [PATCH 09/11] chore: update tests to use the sampler After adding @DictionaryProvider to IntegralMutatorFactory, the selection of mutation functions now does an addition step that runs through weightedSampler, that selects whether to stay in the selection or do an additional step and select the alias. --- .../jazzer/mutation/mutator/aggregate/RecordMutatorTest.java | 1 + .../jazzer/mutation/mutator/collection/ArrayMutatorTest.java | 1 + .../jazzer/mutation/mutator/collection/ListMutatorTest.java | 3 +++ .../jazzer/mutation/mutator/collection/MapMutatorTest.java | 4 ++++ 4 files changed, 9 insertions(+) diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java index c75c0a69f..aa3a32307 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java @@ -74,6 +74,7 @@ void testSimpleTypesRecord() { // Mutate second component, in range operation, return 23 1, 2, + 0.1, // sampler: we stay in the second function: direct value in range 23L)) { SimpleTypesRecord inited = mutator.init(prng); assertThat(inited).isNotNull(); diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorTest.java index c76c402a3..c8b1fe0d0 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ArrayMutatorTest.java @@ -210,6 +210,7 @@ void testChangeSingleElement() { // mutation choice based on `IntegralMutatorFactory` // 2 == closedRange 2, + 0.1, // sampler: we stay in the closed range mutation // value 55L)) { arr = mutator.mutate(arr, prng); diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorTest.java index b0b98c8e5..abc493534 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/ListMutatorTest.java @@ -181,6 +181,7 @@ void testChangeSingleElement() { // mutation choice based on `IntegralMutatorFactory` // 2 == closedRange 2, + 0.1, // sampler: stay in closedRange mutation // value 55L)) { list = mutator.mutate(list, prng); @@ -205,10 +206,12 @@ void testChangeChunk() { 5, // mutation: 0 == bitflip 0, + 0.1, // sampler: stay in bitflip mutation // shift constant 13, // and again 0, + 0.1, // sampler: stay in bitflip mutation 12)) { list = mutator.mutate(list, prng); } diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorTest.java index 2a9fb21de..69d70063c 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/collection/MapMutatorTest.java @@ -146,10 +146,12 @@ void mapMutateValues() { 3, // uniform pick 2, + 0.1, // sampler: stay in uniform pick // random integer 41L, // uniform pick 2, + 0.1, // sampler: stay in uniform pick // random integer 51L)) { map = mutator.mutate(map, prng); @@ -176,10 +178,12 @@ void mapMutateKeys() { 3, // uniform pick 2, + 0.1, // sampler: stay in uniform pick // integer 7L, // uniform pick 2, + 0.1, // sampler: stay in uniform pick // random integer 8L)) { map = mutator.mutate(map, prng); From e9f25e2cc373145147ac37e5658ea55e7a76cb6d Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 29 Oct 2025 10:16:02 +0100 Subject: [PATCH 10/11] chore: update stress test expectations Some tests have too strict expectations on mutator output and are way off from their true probabilities, and simply running the stress test for more iterations, or with a different seed will result in failed tests due to variance. --- .../jazzer/mutation/mutator/StressTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b0d832112..35f58ee80 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 @@ -837,7 +837,7 @@ void singleParam(int parameter) {} "[Nullable<[Integer, Boolean] -> SimpleRecord>, Nullable<[Integer, Boolean] ->" + " SimpleRecord>] -> RepeatedRecord", true, - distinctElementsRatio(0.49), + distinctElementsRatio(0.45), manyDistinctElements()), arguments( new TypeHolder<@NotNull LinkedListNode>() {}.annotatedType(), @@ -845,7 +845,7 @@ void singleParam(int parameter) {} + " LinkedListNode)>] -> LinkedListNode", false, // Low due to recursion breaking initializing nested records to null. - distinctElementsRatio(0.23), + distinctElementsRatio(0.22), manyDistinctElements()), arguments( new TypeHolder<@NotNull SetterBasedBeanWithParent>() {}.annotatedType(), @@ -858,7 +858,7 @@ void singleParam(int parameter) {} "[Nullable LinkedListBean)>, Integer] -> LinkedListBean", false, // Low due to recursion breaking initializing nested structs to null. - distinctElementsRatio(0.22), + distinctElementsRatio(0.21), manyDistinctElements()), arguments( new TypeHolder<@NotNull ImmutableBuilder>() {}.annotatedType(), From 84707f2d61c72846499a334f60f77711d7dce203 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 29 Oct 2025 12:30:34 +0100 Subject: [PATCH 11/11] chore: increase test timeout Changing the usage of PRNG in the mutators can affect duration of some tests. Slow GH runners are especially affected. --- tests/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 2c8d7402f..e98d44a8e 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -638,6 +638,7 @@ java_fuzz_target_test( java_fuzz_target_test( name = "MutatorComplexProtoFuzzer", + timeout = "long", srcs = ["src/test/java/com/example/MutatorComplexProtoFuzzer.java"], allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium"], fuzzer_args = [