diff --git a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java index f758644cd..79159d176 100644 --- a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java +++ b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java @@ -50,9 +50,9 @@ public class ArgumentsMutatorFuzzTest { static List mutators = methods.stream() .map( - m -> - ArgumentsMutator.forMethod(Mutators.newFactory(), m) - .orElseThrow(() -> new IllegalArgumentException("Invalid method: " + m))) + method -> + ArgumentsMutator.forMethod(Mutators.newFactory(), method) + .orElseThrow(() -> new IllegalArgumentException("Invalid method: " + method))) .collect(Collectors.toList()); static { 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..a9175cee2 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.ValuePool; import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; import com.code_intelligence.jazzer.mutation.api.PseudoRandom; import com.code_intelligence.jazzer.mutation.api.SerializingMutator; @@ -31,6 +32,8 @@ import com.code_intelligence.jazzer.mutation.engine.SeededPseudoRandom; import com.code_intelligence.jazzer.mutation.mutator.Mutators; import com.code_intelligence.jazzer.mutation.support.Preconditions; +import com.code_intelligence.jazzer.mutation.support.TypeSupport; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import com.code_intelligence.jazzer.utils.Log; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -75,7 +78,7 @@ private static String prettyPrintMethod(Method method) { } public static ArgumentsMutator forMethodOrThrow(Method method) { - return forMethod(Mutators.newFactory(), method) + return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method) .orElseThrow( () -> new IllegalArgumentException( @@ -83,7 +86,7 @@ public static ArgumentsMutator forMethodOrThrow(Method method) { } public static Optional forMethod(Method method) { - return forMethod(Mutators.newFactory(), method); + return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method); } public static Optional forMethod( @@ -97,11 +100,20 @@ public static Optional forMethod( Log.error(validationError.getMessage()); throw validationError; } + + ValuePool[] valuePools = method.getAnnotationsByType(ValuePool.class); + return toArrayOrEmpty( stream(method.getAnnotatedParameterTypes()) .map( type -> { - Optional> mutator = mutatorFactory.tryCreate(type); + // Forward all @ValuePool annotations of the fuzz test method to each + // arg. + AnnotatedType t = type; + for (ValuePool dict : valuePools) { + 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..7f5901874 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel @@ -12,6 +12,7 @@ java_library( "//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/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/ValuePool.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java new file mode 100644 index 000000000..a2da6d169 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java @@ -0,0 +1,94 @@ +/* + * 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 values to user-selected mutator types to start fuzzing from. 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 MyFuzzTargets {
+ *
+ *   static Stream valluesVisibleByAllArgumentMutators() {
+ *     return Stream.of("example1", "example2", "example3", 1232187321, -182371);
+ *   }
+ *
+ *   static Stream valuesVisibleOnlyByAnotherInput() {
+ *     return Stream.of("code-intelligence.com", "secret.url.1082h3u21ibsdsazuvbsa.com");
+ *   }
+ *
+ *   @ValuePool("valuesVisibleByAllArgumentMutators")
+ *   @FuzzTest
+ *   public void fuzzerTestOneInput(String input, @ValuePool("valuesVisibleOnlyByAnotherInput") 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 ValuePool} annotation that uses {@code provide} and the + * parameter-level {@code ValuePool} annotation that uses {@code provideSomethingElse}. + */ +@Target({ElementType.METHOD, TYPE_USE}) +@Retention(RUNTIME) +@IgnoreRecursiveConflicts +@PropertyConstraint +public @interface ValuePool { + /** + * Specifies supplier methods that generate 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 ValuePool} will be used with probability {@code 1/p} by the mutator responsible for + * fitting types. Not all mutators respect this probability. + */ + double p() default 0.1; + + /** + * Defines the scope of the annotation. Possible values are defined in {@link + * com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. By default it's {@code + * RECURSIVE}. + */ + String constraint() default RECURSIVE; +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java index c0b810395..97bcab408 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java @@ -27,18 +27,23 @@ import com.code_intelligence.jazzer.mutation.mutator.libfuzzer.LibFuzzerMutators; import com.code_intelligence.jazzer.mutation.mutator.proto.ProtoMutators; import com.code_intelligence.jazzer.mutation.mutator.time.TimeMutators; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import java.util.stream.Stream; public final class Mutators { private Mutators() {} public static ExtendedMutatorFactory newFactory() { + return newFactory(null); + } + + public static ExtendedMutatorFactory newFactory(ValuePoolRegistry valuePoolRegistry) { return ChainedMutatorFactory.of( new IdentityCache(), NonNullableMutators.newFactories(), - LangMutators.newFactories(), + LangMutators.newFactories(valuePoolRegistry), CollectionMutators.newFactories(), - ProtoMutators.newFactories(), + ProtoMutators.newFactories(valuePoolRegistry), LibFuzzerMutators.newFactories(), TimeMutators.newFactories(), // Keep generic aggregate mutators last in case a concrete type is also an aggregate type. 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/LangMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java index 0fcf278fa..7dc909ff1 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java @@ -17,15 +17,21 @@ package com.code_intelligence.jazzer.mutation.mutator.lang; import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import java.util.stream.Stream; public final class LangMutators { private LangMutators() {} public static Stream newFactories() { + return newFactories(null); + } + + public static Stream newFactories(ValuePoolRegistry valuePoolRegistry) { return Stream.of( // DON'T EVER SORT THESE! The order is important for the mutator engine to work correctly. new NullableMutatorFactory(), + new ValuePoolMutatorFactory(valuePoolRegistry), new BooleanMutatorFactory(), new FloatingPointMutatorFactory(), new IntegralMutatorFactory(), 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..7d33c74e5 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,9 @@ 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.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; diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java new file mode 100644 index 000000000..c9231f13c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java @@ -0,0 +1,188 @@ +/* + * 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.mutator.lang; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +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.support.TypeHolder; +import com.code_intelligence.jazzer.mutation.support.TypeSupport; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ValuePoolMutatorFactory implements MutatorFactory { + /** Types annotated with this marker wil not be re-wrapped by this factory. */ + @Target({TYPE_USE}) + @Retention(RUNTIME) + private @interface ValuePoolMarker {} + + public static final Annotation VALUE_POOL_MARKER = + new TypeHolder<@ValuePoolMarker String>() {}.annotatedType() + .getAnnotation(ValuePoolMarker.class); + + private final ValuePoolRegistry valuePoolRegistry; + + ValuePoolMutatorFactory(ValuePoolRegistry valuePoolRegistry) { + this.valuePoolRegistry = valuePoolRegistry; + } + + @Override + public Optional> tryCreate( + AnnotatedType type, ExtendedMutatorFactory factory) { + if (type.getAnnotation(ValuePoolMarker.class) != null) { + return Optional.empty(); + } + AnnotatedType markedType = TypeSupport.withExtraAnnotations(type, VALUE_POOL_MARKER); + return factory + .tryCreate(markedType) + .map(mutator -> ValuePoolMutator.wrapIfValuesExist(markedType, mutator, valuePoolRegistry)); + } + + private static final class ValuePoolMutator extends SerializingMutator { + private final SerializingMutator mutator; + private final List userValues; + private final double poolUsageProbability; + + ValuePoolMutator( + SerializingMutator mutator, List userValues, double poolUsageProbability) { + this.mutator = mutator; + this.userValues = userValues; + this.poolUsageProbability = poolUsageProbability; + } + + @SuppressWarnings("unchecked") + static SerializingMutator wrapIfValuesExist( + AnnotatedType type, SerializingMutator mutator, ValuePoolRegistry valuePoolRegistry) { + + if (valuePoolRegistry == null) { + return mutator; + } + + Optional> rawUserValues = valuePoolRegistry.extractRawValues(type); + if (!rawUserValues.isPresent()) { + return mutator; + } + + List userValues = + rawUserValues + .get() + .filter(value -> isSerializationStable(mutator, value)) + .map(value -> mutator.detach((T) value)) + .collect(Collectors.toList()); + + if (userValues.isEmpty()) { + return mutator; + } + + double p = valuePoolRegistry.extractFirstProbability(type); + return new ValuePoolMutator<>(mutator, userValues, p); + } + + @SuppressWarnings("unchecked") + private static boolean isSerializationStable(SerializingMutator mutator, Object value) { + try { + ByteArrayOutputStream original = new ByteArrayOutputStream(); + mutator.write((T) value, new DataOutputStream(original)); + byte[] serialized = original.toByteArray(); + + DataInputStream in = new DataInputStream(new ByteArrayInputStream(serialized)); + T deserialized = mutator.read(in); + + ByteArrayOutputStream roundTrip = new ByteArrayOutputStream(); + mutator.write(deserialized, new DataOutputStream(roundTrip)); + byte[] reserialized = roundTrip.toByteArray(); + + return Arrays.equals(serialized, reserialized); + } catch (Exception e) { + return false; + } + } + + @Override + public String toDebugString(Predicate isInCycle) { + return String.format( + "{vals: %d p: %.2f}-%s", + userValues.size(), poolUsageProbability, mutator.toDebugString(isInCycle)); + } + + @Override + public T read(DataInputStream in) throws IOException { + return mutator.read(in); + } + + @Override + public void write(T value, DataOutputStream out) throws IOException { + mutator.write(value, out); + } + + @Override + public T detach(T value) { + return mutator.detach(value); + } + + @Override + protected boolean computeHasFixedSize() { + return mutator.hasFixedSize(); + } + + @Override + public T init(PseudoRandom prng) { + if (prng.closedRange(0.0, 1.0) < poolUsageProbability) { + return prng.pickIn(userValues); + } else { + return mutator.init(prng); + } + } + + @Override + public T mutate(T value, PseudoRandom prng) { + if (prng.closedRange(0.0, 1.0) < poolUsageProbability) { + if (prng.choice()) { + return prng.pickIn(userValues); + } else { + // treat the value from valuePool as a starting point for mutation + return mutator.mutate(prng.pickIn(userValues), prng); + } + } + return mutator.mutate(value, prng); + } + + @Override + public T crossOver(T value, T otherValue, PseudoRandom prng) { + return mutator.crossOver(value, otherValue, prng); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java index 48c717b3c..d4f70c5dc 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/BuilderMutatorFactory.java @@ -56,6 +56,7 @@ import com.code_intelligence.jazzer.mutation.mutator.collection.CollectionMutators; import com.code_intelligence.jazzer.mutation.mutator.lang.LangMutators; import com.code_intelligence.jazzer.mutation.support.Preconditions; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import com.google.protobuf.Any; import com.google.protobuf.CodedInputStream; import com.google.protobuf.Descriptors.Descriptor; @@ -87,6 +88,11 @@ import java.util.stream.Stream; public final class BuilderMutatorFactory implements MutatorFactory { + final ValuePoolRegistry valuePoolRegistry; + + public BuilderMutatorFactory(ValuePoolRegistry valuePoolRegistry) { + this.valuePoolRegistry = valuePoolRegistry; + } // Generous size limit for decoded protobuf messages. This is necessary to guard against OOM // errors when the corpus format changes e.g. due to a change in the fuzz test signature. @@ -175,7 +181,8 @@ private ExtendedMutatorFactory withDescriptorDependentMutatorFactoryIfNeeded( // to follow constructors or builders of the EnumValueDescriptor class. return ChainedMutatorFactory.of( Stream.concat( - Stream.concat(LangMutators.newFactories(), CollectionMutators.newFactories()), + Stream.concat( + LangMutators.newFactories(valuePoolRegistry), CollectionMutators.newFactories()), Stream.of(enumFactory))); } else if (field.getJavaType() == JavaType.MESSAGE) { if (field.isMapField()) { diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java index ec7b4b9a7..cace93938 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/proto/ProtoMutators.java @@ -17,16 +17,23 @@ package com.code_intelligence.jazzer.mutation.mutator.proto; import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.support.ValuePoolRegistry; import java.util.stream.Stream; public final class ProtoMutators { private ProtoMutators() {} public static Stream newFactories() { + return newFactories(null); + } + + public static Stream newFactories(ValuePoolRegistry valuePoolRegistry) { try { Class.forName("com.google.protobuf.Message"); return Stream.of( - new ByteStringMutatorFactory(), new MessageMutatorFactory(), new BuilderMutatorFactory()); + new ByteStringMutatorFactory(), + new MessageMutatorFactory(), + new BuilderMutatorFactory(valuePoolRegistry)); } catch (ClassNotFoundException e) { return Stream.empty(); } 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/support/ValuePoolRegistry.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java new file mode 100644 index 000000000..1fb544717 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java @@ -0,0 +1,141 @@ +/* + * 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.ValuePool; +import com.code_intelligence.jazzer.utils.Log; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ValuePoolRegistry { + private final Map>> pools; + private final Method fuzzTestMethod; + + public ValuePoolRegistry(Method fuzzTestMethod) { + this.fuzzTestMethod = fuzzTestMethod; + this.pools = extractValueSuppliers(fuzzTestMethod); + } + + /** + * Extract probability of the very first {@code ValuePool} annotation on the given type. The + * {@code @ValuePool} 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 @ValuePool} annotations appended later to this type because of {@code + * PropertyConstraint.RECURSIVE}, are ignored. Callers should ensure that at least one + * {@code @ValuePool} annotation is present on the type. + */ + public double extractFirstProbability(AnnotatedType type) { + // If there are several @ValuePool annotations on the type, this will take the most + // immediate one, because @ValuePool is not repeatable. + ValuePool[] dictObj = type.getAnnotationsByType(ValuePool.class); + if (dictObj.length == 0) { + // If we are here, it's a bug in the caller. + throw new IllegalStateException("Expected to find @ValuePool, but found none."); + } + double p = dictObj[0].p(); + require(p >= 0.0 && p <= 1.0, "@ValuePool p must be in [0.0, 1.0], but was " + p); + return p; + } + + public Optional> extractRawValues(AnnotatedType type) { + String[] poolNames = + Arrays.stream(type.getAnnotations()) + .filter(annotation -> annotation instanceof ValuePool) + .map(annotation -> (ValuePool) annotation) + .map(ValuePool::value) + .flatMap(Arrays::stream) + .toArray(String[]::new); + + if (poolNames.length == 0) { + return Optional.empty(); + } + + return Optional.of( + Arrays.stream(poolNames) + .flatMap( + name -> { + Supplier> supplier = pools.get(name); + if (supplier == null) { + throw new IllegalStateException( + "No method named '" + + name + + "' found for @ValuePool on type " + + type.getType().getTypeName() + + " in fuzz test method " + + fuzzTestMethod.getName() + + ". Available provider methods: " + + String.join(", ", pools.keySet())); + } + return supplier.get(); + }) + .distinct()); + } + + private static Map>> extractValueSuppliers(Method fuzzTestMethod) { + return Arrays.stream(fuzzTestMethod.getDeclaringClass().getDeclaredMethods()) + .filter(m -> m.getParameterCount() == 0) + .filter(m -> Stream.class.equals(m.getReturnType())) + .filter(m -> Modifier.isStatic(m.getModifiers())) + .collect(Collectors.toMap(Method::getName, ValuePoolRegistry::createLazyStreamSupplier)); + } + + private static Supplier> createLazyStreamSupplier(Method method) { + return new Supplier>() { + private volatile List cachedData = null; + + @Override + public Stream get() { + if (cachedData == null) { + synchronized (this) { + if (cachedData == null) { + cachedData = loadDataFromMethod(method); + } + } + if (cachedData.isEmpty()) { + Log.warn("ValuePool method " + method.getName() + " provided no values."); + return Stream.empty(); + } + } + + return cachedData.stream(); + } + }; + } + + private static List loadDataFromMethod(Method method) { + method.setAccessible(true); + try { + Stream stream = (Stream) method.invoke(null); + return stream.collect(Collectors.toList()); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot access method " + method.getName(), e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error invoking method " + method.getName(), e.getCause()); + } + } +} 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..0349b934e --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/utils/IgnoreRecursiveConflicts.java @@ -0,0 +1,38 @@ +/* + * 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 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 can see 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/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index 29e0aad35..07eb3842e 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 @@ -582,6 +582,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[]", 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 75b5d57b9..d69a7f0a2 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 @@ -30,6 +30,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/ValuePoolsTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java new file mode 100644 index 000000000..066324b44 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java @@ -0,0 +1,176 @@ +/* + * 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.ValuePool; +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 ValuePoolsTest { + + /* Dummy fuzz test method to add to MutatorRuntime. */ + public void dummyFuzzTestMethod() {} + + private static final ValuePoolRegistry valuePools; + + static { + try { + valuePools = new ValuePoolRegistry(ValuePoolsTest.class.getMethod("dummyFuzzTestMethod")); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + public static Stream myPool() { + return Stream.of("value1", "value2", "value3"); + } + + public static Stream myPool2() { + return Stream.of("value1", "value2", "value3", "value4"); + } + + @Test + void testExtractFirstProbability_Default() { + AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); + double p = valuePools.extractFirstProbability(type); + assertThat(p).isEqualTo(0.1); + } + + @Test + void testExtractFirstProbability_OneUserDefined() { + AnnotatedType type = + new TypeHolder<@ValuePool(value = "myPool2", p = 0.2) String>() {}.annotatedType(); + double p = valuePools.extractFirstProbability(type); + assertThat(p).isEqualTo(0.2); + } + + @Test + void testExtractFirstProbability_TwoWithLastUsed() { + AnnotatedType type = + TypeSupport.withExtraAnnotations( + new TypeHolder<@ValuePool(value = "myPool", p = 0.2) String>() {}.annotatedType(), + withValuePoolImplementation(new String[] {"myPool2"}, 0.3)); + double p = valuePools.extractFirstProbability(type); + assertThat(p).isEqualTo(0.2); + } + + @Test + void testExtractRawValues_OneAnnotation() { + AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); + Optional> elements = valuePools.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3"); + } + + @Test + void testExtractProviderStreams_JoinStreamsInOneProvider() { + AnnotatedType type = + new TypeHolder<@ValuePool({"myPool", "myPool2"}) String>() {}.annotatedType(); + Optional> elements = valuePools.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + } + + @Test + void testExtractRawValues_JoinTwoFromOne() { + AnnotatedType type = + new TypeHolder<@ValuePool({"myPool", "myPool2"}) String>() {}.annotatedType(); + Optional> elements = valuePools.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + } + + @Test + void testExtractRawValues_JoinFromTwoSeparateAnnotations() { + AnnotatedType type = + TypeSupport.withExtraAnnotations( + new TypeHolder<@ValuePool("myPool2") String>() {}.annotatedType(), + withValuePoolImplementation(new String[] {"myPool"}, 5)); + Optional> elements = valuePools.extractRawValues(type); + assertThat(elements).isPresent(); + assertThat(elements.get()).containsExactly("value1", "value2", "value3", "value4"); + } + + private static ValuePool withValuePoolImplementation(String[] value, double p) { + return withValuePoolImplementation(value, p, PropertyConstraint.RECURSIVE); + } + + private static ValuePool withValuePoolImplementation( + String[] value, double p, String constraint) { + return new ValuePool() { + @Override + public String[] value() { + return value; + } + + @Override + public double p() { + return p; + } + + @Override + public String constraint() { + return constraint; + } + + @Override + public Class annotationType() { + return ValuePool.class; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ValuePool)) { + return false; + } + ValuePool other = (ValuePool) o; + return Arrays.equals(this.value(), other.value()) + && this.p() == other.p() + && this.constraint().equals(other.constraint()); + } + + @Override + public int hashCode() { + int hash = 0; + hash += Arrays.hashCode(value()) * 127; + hash += Double.hashCode(p()) * 31 * 127; + hash += constraint().hashCode() * 127; + return hash; + } + + @Override + public String toString() { + return "@" + + ValuePool.class.getName() + + "(value={" + + String.join(", ", value()) + + "}, p=" + + p() + + ", constraint=" + + constraint() + + ")"; + } + }; + } +}