Skip to content

Commit b71ebec

Browse files
committed
feat: @DictionaryProvider - propagate values to type mutators
This is just the enabling work. Methods and types annotated by @DictionaryProvider recursively propagate this annotation down the type hierarchy by default (can set to be for the annotated type only). Any mutator can now be adapted to use the user-provided values this annotation points to.
1 parent a5feff0 commit b71ebec

File tree

7 files changed

+399
-1
lines changed

7 files changed

+399
-1
lines changed

src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ java_library(
3838
"//examples/junit/src/test/java/com/example:__pkg__",
3939
"//selffuzz/src/test/java/com/code_intelligence/selffuzz:__subpackages__",
4040
"//src/test/java/com/code_intelligence/jazzer/junit:__pkg__",
41+
"//src/test/java/com/code_intelligence/jazzer/mutation/support:__pkg__",
4142
],
4243
exports = [
4344
":lifecycle",

src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static java.util.Arrays.stream;
2424
import static java.util.stream.Collectors.joining;
2525

26+
import com.code_intelligence.jazzer.mutation.annotation.DictionaryProvider;
2627
import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory;
2728
import com.code_intelligence.jazzer.mutation.api.PseudoRandom;
2829
import com.code_intelligence.jazzer.mutation.api.SerializingMutator;
@@ -32,6 +33,7 @@
3233
import com.code_intelligence.jazzer.mutation.mutator.Mutators;
3334
import com.code_intelligence.jazzer.mutation.runtime.MutatorRuntime;
3435
import com.code_intelligence.jazzer.mutation.support.Preconditions;
36+
import com.code_intelligence.jazzer.mutation.support.TypeSupport;
3537
import com.code_intelligence.jazzer.utils.Log;
3638
import java.io.ByteArrayInputStream;
3739
import java.io.IOException;
@@ -99,12 +101,19 @@ public static Optional<ArgumentsMutator> forMethod(
99101
throw validationError;
100102
}
101103
MutatorRuntime runtime = MutatorRuntime.forFuzzTestMethod(method);
104+
DictionaryProvider[] typeDictionaries = method.getAnnotationsByType(DictionaryProvider.class);
102105
return toArrayOrEmpty(
103106
stream(method.getAnnotatedParameterTypes())
104107
.map(
105108
type -> {
109+
// Forward all DictionaryProvider annotations of the fuzz test method to each
110+
// arg.
111+
AnnotatedType t = type;
112+
for (DictionaryProvider dict : typeDictionaries) {
113+
t = TypeSupport.withExtraAnnotations(t, dict);
114+
}
106115
Optional<SerializingMutator<?>> mutator =
107-
mutatorFactory.tryCreate(runtime, type);
116+
mutatorFactory.tryCreate(runtime, t);
108117
if (!mutator.isPresent()) {
109118
Log.error(
110119
String.format(

src/main/java/com/code_intelligence/jazzer/mutation/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ java_library(
1313
"//src/main/java/com/code_intelligence/jazzer/mutation/mutator",
1414
"//src/main/java/com/code_intelligence/jazzer/mutation/runtime",
1515
"//src/main/java/com/code_intelligence/jazzer/mutation/support",
16+
"//src/main/java/com/code_intelligence/jazzer/mutation/utils",
1617
"//src/main/java/com/code_intelligence/jazzer/utils:log",
1718
],
1819
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2024 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.mutation.annotation;
18+
19+
import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.RECURSIVE;
20+
import static java.lang.annotation.ElementType.TYPE_USE;
21+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
22+
23+
import com.code_intelligence.jazzer.mutation.utils.IgnoreRecursiveConflicts;
24+
import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint;
25+
import java.lang.annotation.ElementType;
26+
import java.lang.annotation.Retention;
27+
import java.lang.annotation.Target;
28+
29+
/**
30+
* Specifies provider methods that generate dictionary values for fuzzing the annotated method or
31+
* type. The specified provider methods must be static, parameterless, and return a {@code Stream
32+
* <?>} of values. The values don't need to match the type of the annotated method or parameter
33+
* exactly. The mutation framework will extract only the values that are compatible with the target
34+
* type.
35+
*
36+
* <p>The annotation can be applied to methods and parameters of any type. When applied to complex
37+
* types (e.g. custom classes), the annotation is expected to apply to all nested fields unless
38+
* specified otherwise via the {@link #constraint()} attribute.
39+
*
40+
* <p>Example usage:
41+
*
42+
* <pre>{@code
43+
* public class MyFuzzTarget {
44+
*
45+
* public Stream<?> provide() {
46+
* return Stream.of("example1", "example2", "example3", 1232187321, -182371);
47+
* }
48+
*
49+
* public Stream<?> provideSomethingElse() {
50+
* return Stream.of("code-intelligence.com", "secret.url.1082h3u21ibsdsazuvbsa.com");
51+
* }
52+
*
53+
* @DictionaryProvider("provide")
54+
* @FuzzTest
55+
* public void fuzzerTestOneInput(String input, @DictionaryProvider("provideSomethingElse") String anotherInput) {
56+
* // Fuzzing logic here
57+
* }
58+
* }
59+
* }</pre>
60+
*
61+
* In this example, the mutator for the String parameter {@code input} of the fuzz test method
62+
* {@code fuzzerTestOneInput} will be using the values returned by {@code provide} method during
63+
* mutation, while the mutator for String {@code anotherInput} will use values from both methods:
64+
* from the method-level {@code DictionaryProvider} annotation that uses {@code provide} and the
65+
* parameter-level {@code DictionaryProvider} annotation that uses {@code provideSomethingElse}.
66+
*/
67+
@Target({ElementType.METHOD, TYPE_USE})
68+
@Retention(RUNTIME)
69+
@IgnoreRecursiveConflicts
70+
@PropertyConstraint
71+
public @interface DictionaryProvider {
72+
String[] value() default {""};
73+
74+
/**
75+
* This {@code DictionaryProvider} will be used with probability {@code 1/p} by the mutator
76+
* responsible for fitting types. Not all mutators respect this probability.
77+
*/
78+
int pInv() default 10;
79+
80+
/**
81+
* Defines the scope of the annotation. Possible values are defined in {@link
82+
* com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. It is convenient to use {@code
83+
* RECURSIVE} as the default value here, as dictionary objects are typically used for complex
84+
* types (e.g. custom classes) where the annotation is placed directly on the method or parameter
85+
* and is expected to apply to all nested fields.
86+
*/
87+
String constraint() default RECURSIVE;
88+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.mutation.support;
18+
19+
import static com.code_intelligence.jazzer.mutation.support.Preconditions.require;
20+
21+
import com.code_intelligence.jazzer.mutation.annotation.DictionaryProvider;
22+
import com.code_intelligence.jazzer.mutation.runtime.MutatorRuntime;
23+
import java.lang.reflect.AnnotatedType;
24+
import java.lang.reflect.InvocationTargetException;
25+
import java.lang.reflect.Method;
26+
import java.util.Arrays;
27+
import java.util.HashSet;
28+
import java.util.Map;
29+
import java.util.Optional;
30+
import java.util.stream.Collectors;
31+
import java.util.stream.Stream;
32+
33+
public class DictionaryProviderSupport {
34+
35+
/**
36+
* Extract inverse probability of the very first {@code DictionaryProvider} annotation on the
37+
* given type. The {@code @DictionaryProvider} annotation directly on the type is preferred; if
38+
* there is none, the first one appended because of {@code PropertyConstraint.RECURSIVE} is used.
39+
* Any further {@code @DictionaryProvider} annotations appended later to this type because of
40+
* {@code PropertyConstraint.RECURSIVE}, are ignored. Callers should ensure that at least one
41+
* {@code @DictionaryProvider} annotation is present on the type.
42+
*/
43+
public static int extractFirstInvProbability(AnnotatedType type) {
44+
// If there are several @DictionaryProvider annotations on the type, this will take the most
45+
// immediate one, because @DictionaryProvider is not repeatable.
46+
DictionaryProvider[] dictObj = type.getAnnotationsByType(DictionaryProvider.class);
47+
if (dictObj.length == 0) {
48+
// If we are here, it's a bug in the caller.
49+
throw new IllegalStateException("Expected to find @DictionaryProvider, but found none.");
50+
}
51+
int pInv = dictObj[0].pInv();
52+
require(pInv >= 2, "@DictionaryProvider.pInv must be at least 2");
53+
return pInv;
54+
}
55+
56+
/** Extract the provider streams using MutatorRuntime */
57+
public static Optional<Stream<?>> extractRawValues(MutatorRuntime runtime, AnnotatedType type) {
58+
DictionaryProvider[] providers =
59+
Arrays.stream(type.getAnnotations())
60+
.filter(a -> a instanceof DictionaryProvider)
61+
.map(a -> (DictionaryProvider) a)
62+
.toArray(DictionaryProvider[]::new);
63+
if (providers.length == 0) {
64+
return Optional.empty();
65+
}
66+
HashSet<String> providerMethodNames =
67+
Arrays.stream(providers)
68+
.map(DictionaryProvider::value)
69+
.flatMap(Arrays::stream)
70+
.filter(name -> !name.isEmpty())
71+
.collect(Collectors.toCollection(HashSet::new));
72+
if (providerMethodNames.isEmpty()) {
73+
return Optional.empty();
74+
}
75+
Map<String, Method> fuzzTestMethods =
76+
Arrays.stream(runtime.fuzzTestMethod.getDeclaringClass().getDeclaredMethods())
77+
.filter(m -> m.getParameterCount() == 0)
78+
.filter(m -> m.getReturnType().equals(Stream.class))
79+
.filter(
80+
m ->
81+
(m.getModifiers()
82+
& (java.lang.reflect.Modifier.PUBLIC
83+
| java.lang.reflect.Modifier.STATIC))
84+
== (java.lang.reflect.Modifier.PUBLIC | java.lang.reflect.Modifier.STATIC))
85+
.collect(Collectors.toMap(Method::getName, m -> m));
86+
return Optional.ofNullable(
87+
providerMethodNames.stream()
88+
.flatMap(
89+
name -> {
90+
Method m = fuzzTestMethods.get(name);
91+
if (m == null) {
92+
throw new IllegalStateException(
93+
"No method named "
94+
+ name
95+
+ " with signature 'public static Stream<?> "
96+
+ name
97+
+ "()' found in class "
98+
+ runtime.fuzzTestMethod.getDeclaringClass().getName());
99+
}
100+
try {
101+
m.setAccessible(true);
102+
return (Stream<?>) m.invoke(null);
103+
} catch (IllegalAccessException e) {
104+
throw new RuntimeException(e);
105+
} catch (InvocationTargetException e) {
106+
throw new RuntimeException(e);
107+
}
108+
})
109+
.distinct());
110+
}
111+
}

src/test/java/com/code_intelligence/jazzer/mutation/support/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ java_test_suite(
3030
runner = "junit5",
3131
deps = [
3232
":test_support",
33+
"//deploy:jazzer-project",
34+
"//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test",
3335
"//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
3436
"//src/main/java/com/code_intelligence/jazzer/mutation/support",
3537
"//src/main/java/com/code_intelligence/jazzer/mutation/utils",

0 commit comments

Comments
 (0)