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 86bfdd67c..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; @@ -30,7 +31,9 @@ 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.mutation.support.TypeSupport; import com.code_intelligence.jazzer.utils.Log; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -97,11 +100,19 @@ 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( 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 73e0472a6..2cbedab19 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel @@ -11,7 +11,9 @@ 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/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/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/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/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; 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/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; +} 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/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/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java index 2480d4e93..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; @@ -37,9 +38,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; @@ -575,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; @@ -687,4 +694,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(); + } + } } 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 {} 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); + } + } +} 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..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 @@ -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[]", @@ -831,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(), @@ -839,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(), @@ -852,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(), 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); 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() + + ")"; + } + }; + } +} diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 22dca2a3b..e98d44a8e 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"], @@ -616,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 = [ 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(); + } +}