From bec00ccc310476806aab42a6bfd11029f52598d2 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Mon, 19 Jan 2026 16:58:14 +0100 Subject: [PATCH 1/9] feat: cache mutators and value pools for each method Don't generate the same mutator for the same fuzz test method multiple times. Before this change, a mutator was generated for each crash file in fuzzing mode. --- .../jazzer/mutation/ArgumentsMutator.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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 db42c791e..27490c3ee 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/ArgumentsMutator.java @@ -43,13 +43,17 @@ import java.lang.reflect.AnnotatedType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; public final class ArgumentsMutator { private final ExtendedMutatorFactory mutatorFactory; private final Method method; private final InPlaceProductMutator productMutator; + private static final Map mutatorsCache = new ConcurrentHashMap<>(); + private Object[] arguments; /** @@ -78,15 +82,14 @@ private static String prettyPrintMethod(Method method) { } public static ArgumentsMutator forMethodOrThrow(Method method) { - return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method) - .orElseThrow( - () -> - new IllegalArgumentException( - "Failed to construct mutator for " + prettyPrintMethod(method))); - } - - public static Optional forMethod(Method method) { - return forMethod(Mutators.newFactory(new ValuePoolRegistry(method)), method); + return mutatorsCache.computeIfAbsent( + method, + m -> + forMethod(Mutators.newFactory(new ValuePoolRegistry(m)), m) + .orElseThrow( + () -> + new IllegalArgumentException( + "Failed to construct mutator for " + prettyPrintMethod(m)))); } public static Optional forMethod( From 826bb747e3256ae4d2fe81f3f109f0de93ce31d8 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Fri, 19 Dec 2025 17:00:29 +0100 Subject: [PATCH 2/9] feat: ValuePool can load files that match directory patterns --- .../jazzer/junit/FuzzTestExecutor.java | 2 + .../jazzer/mutation/annotation/ValuePool.java | 27 +- .../mutator/lang/ValuePoolMutatorFactory.java | 10 +- .../mutation/support/ValuePoolRegistry.java | 167 ++++++++- .../mutation/support/ValuePoolsTest.java | 321 +++++++++++++++++- 5 files changed, 484 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java index 49294ad8c..d88cfc325 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -178,6 +178,8 @@ private static Path addInputAndSeedDirs( Paths.get(context.getConfigurationParameter("jazzer.internal.basedir").orElse("")) .toAbsolutePath(); + System.setProperty("jazzer.internal.basedir", baseDir.toString()); + // Use the specified corpus dir, if given, otherwise store the generated corpus in a per-class // directory under the project root. // The path is specified relative to the current working directory, which with JUnit is the 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 index ce636c515..4e78ce175 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java @@ -27,7 +27,7 @@ import java.lang.annotation.Target; /** - * Provides values to user-selected mutator types to start fuzzing from. + * Provides values to user-selected types that will be used during mutation. * *

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 @@ -72,7 +72,28 @@ * don't need to match the type of the annotated method or parameter. The mutation framework will * extract only the values that are compatible with the target type. */ - String[] value(); + String[] value() default {}; + + /** + * Specifies glob patterns matching files that should be provided as {@code byte []} to the + * annotated type. The syntax follows closely to Java's {@link + * java.nio.file.FileSystem#getPathMatcher(String) PathMatcher} "glob:" syntax. + * + *

Relative glob patterns are resolved against the working directory. + * + *

Examples: + * + *

    + *
  • {@code *.jpeg} - matches all jpegs in the working directory + *
  • {@code **.xml} - matches all xml files recursively + *
  • {@code src/test/resources/dict/*.txt} - matches txt files in a specific directory + *
  • {@code /absolute/path/to/some/directory/**} - matches all files in an absolute path + * recursively + *
  • {@code {"*.jpg", "**.png"}} - matches all jpg in the working directory, and png files + * recursively + *
+ */ + String[] files() default {}; /** * This {@code ValuePool} will be used with probability {@code p} by the mutator responsible for @@ -82,7 +103,7 @@ /** * Defines the scope of the annotation. Possible values are defined in {@link - * com.code_intelligence.jazzer.mutation.utils.PropertyConstraint}. By default it's {@code + * 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/lang/ValuePoolMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ValuePoolMutatorFactory.java index 3c2c94877..ddf618639 100644 --- 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 @@ -41,7 +41,6 @@ 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. */ @@ -91,14 +90,9 @@ static SerializingMutator wrapIfValuesExist( return mutator; } - Optional> rawUserValues = valuePoolRegistry.extractRawValues(type); - if (!rawUserValues.isPresent()) { - return mutator; - } - List userValues = - rawUserValues - .get() + valuePoolRegistry + .extractUserValues(type) // Values whose round trip serialization is not stable violate either some user // annotations on the type (e.g. @InRange), or the default mutator limits (e.g. // default List size limits) and are therefore not suitable for inclusion in the value 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 index 8b7802122..43f1c0dd0 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java @@ -20,11 +20,23 @@ import com.code_intelligence.jazzer.mutation.annotation.ValuePool; import com.code_intelligence.jazzer.utils.Log; +import java.io.File; +import java.io.IOException; import java.lang.reflect.AnnotatedType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -33,12 +45,25 @@ import java.util.stream.Stream; public class ValuePoolRegistry { - private final Map>> pools; private final Method fuzzTestMethod; + private final Path baseDir; + private final Map>> pools; + private final Map> pathToBytesCache = new LinkedHashMap<>(); public ValuePoolRegistry(Method fuzzTestMethod) { + this(fuzzTestMethod, computeBaseDir()); + } + + protected ValuePoolRegistry(Method fuzzTestMethod, Path baseDir) { this.fuzzTestMethod = fuzzTestMethod; this.pools = extractValueSuppliers(fuzzTestMethod); + this.baseDir = baseDir; + } + + private static Path computeBaseDir() { + return System.getProperty("jazzer.internal.basedir") == null + ? Paths.get("").toAbsolutePath().normalize() + : Paths.get(System.getProperty("jazzer.internal.basedir")); } /** @@ -62,21 +87,12 @@ public double extractFirstProbability(AnnotatedType type) { return p; } - public Optional> extractRawValues(AnnotatedType type) { - String[] poolNames = - Arrays.stream(type.getAnnotations()) - .filter(annotation -> annotation instanceof ValuePool) - .map(annotation -> (ValuePool) annotation) + public Stream extractUserValues(AnnotatedType type) { + Stream valuesFromSourceMethods = + getValuePoolAnnotations(type).stream() .map(ValuePool::value) .flatMap(Arrays::stream) - .toArray(String[]::new); - - if (poolNames.length == 0) { - return Optional.empty(); - } - - return Optional.of( - Arrays.stream(poolNames) + .filter(name -> !name.isEmpty()) .flatMap( name -> { Supplier> supplier = pools.get(name); @@ -93,7 +109,127 @@ public Optional> extractRawValues(AnnotatedType type) { } return supplier.get(); }) - .distinct()); + .distinct(); + + // Walking patterns and reading files is expensive - we only do it when the mutator target type + // is byte[] + Stream valuesFromFiles = + type.getType() == byte[].class ? extractByteArraysFromPatterns(type) : Stream.empty(); + + return Stream.concat(valuesFromSourceMethods, valuesFromFiles).distinct(); + } + + private Stream extractByteArraysFromPatterns(AnnotatedType type) { + List annotations = getValuePoolAnnotations(type); + + return annotations.stream() + .map(ValuePool::files) + .flatMap(Arrays::stream) + .filter(glob -> !glob.isEmpty()) + .distinct() + .flatMap(glob -> collectPathsForGlob(baseDir, glob)) + .distinct() + .map(this::tryReadFile) + .filter(Optional::isPresent) + .map(Optional::get); + } + + private List getValuePoolAnnotations(AnnotatedType type) { + return Arrays.stream(type.getAnnotations()) + .filter(annotation -> annotation instanceof ValuePool) + .map(annotation -> (ValuePool) annotation) + .collect(Collectors.toList()); + } + + protected static Stream collectPathsForGlob(Path baseDir, String glob) { + int firstGlobChar = indexOfFirstGlobChar(glob); + if (firstGlobChar == -1) { + Path path = Paths.get(glob); + if (!Files.exists(path)) { + return Stream.empty(); + } + return Files.isRegularFile(path) ? Stream.of(path) : Stream.empty(); + } + + String prefix = glob.substring(0, firstGlobChar); + int lastSeparator; + if (File.separatorChar == '\\') { + int lastSlash = prefix.lastIndexOf('/'); + int lastBackSlash = prefix.lastIndexOf('\\'); + lastSeparator = Math.max(lastSlash, lastBackSlash); + } else { + lastSeparator = prefix.lastIndexOf('/'); + } + + // start path is always absolute + Path start; + if (lastSeparator == -1) { + start = baseDir.toAbsolutePath().normalize(); + } else { // absolute + if (Paths.get(prefix).isAbsolute()) { + start = Paths.get(prefix); + } else { + start = baseDir.toAbsolutePath().normalize().resolve(prefix.substring(0, lastSeparator)); + } + } + + if (!Files.exists(start)) { + return Stream.empty(); + } + + String remainingPattern = lastSeparator == -1 ? glob : glob.substring(lastSeparator + 1); + // matcher is always relative to start path + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + remainingPattern); + + List matches = new ArrayList<>(); + try { + Files.walkFileTree( + start, + new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (!Files.isRegularFile(file)) { + return FileVisitResult.CONTINUE; + } + Path relativePath = start.relativize(file); + if (matcher.matches(relativePath)) { + matches.add(file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + return Stream.empty(); + } + return matches.stream(); + } + + private Optional tryReadFile(Path path) { + Path normalizedPath = path.toAbsolutePath().normalize(); + return pathToBytesCache.computeIfAbsent( + normalizedPath, + p -> { + try { + return Optional.of(Files.readAllBytes(path)); + } catch (IOException e) { + return Optional.empty(); + } + }); + } + + private static int indexOfFirstGlobChar(String glob) { + for (int i = 0; i < glob.length(); i++) { + char c = glob.charAt(i); + if (c == '*' || c == '?' || c == '{' || c == '[') { + return i; + } + } + return -1; } private static Map>> extractValueSuppliers(Method fuzzTestMethod) { @@ -121,7 +257,6 @@ public Stream get() { return Stream.empty(); } } - return cachedData.stream(); } }; 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 index 56b9c72f6..2bc204946 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java @@ -24,13 +24,19 @@ import static com.google.common.truth.Truth.assertThat; import com.code_intelligence.jazzer.mutation.annotation.ValuePool; +import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; -import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class ValuePoolsTest { @@ -83,27 +89,27 @@ void testExtractFirstProbability_TwoWithLastUsed() { @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"); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).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"); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).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"); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("value1", "value2", "value3", "value4"); } @Test @@ -112,9 +118,61 @@ void testExtractRawValues_JoinFromTwoSeparateAnnotations() { 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"); + List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); + assertThat(elements).isNotEmpty(); + assertThat(elements).containsExactly("value1", "value2", "value3", "value4"); + } + + @Test + void test_collectPathsForGlob_RelativePattern(@TempDir Path tempDir) throws IOException { + + testCollectPathsForGlob( + tempDir, + "**.json", + "sub/b.json", + "sub/deep/c.json", + "sub/deeper/than/foo.json", + "test/c/d/foo.json"); + } + + @Test + void test_collectPathsForGlob_SubDirectory(@TempDir Path tempDir) throws IOException { + + String relative = "sub/deep/**.txt"; + String absolute = + tempDir.toString() + FileSystems.getDefault().getSeparator() + "sub/deep/**.txt"; + + String[] expected = {"sub/deep/c.txt", "sub/deep/corpus/d.txt"}; + + testCollectPathsForGlob(tempDir, relative, expected); + testCollectPathsForGlob(tempDir, absolute, expected); + } + + @Test + void test_collectPathsForGlob_EqualAbsoluteRelative(@TempDir Path tempDir) throws IOException { + + String relative = "**.json"; + String absolute = tempDir.toString() + FileSystems.getDefault().getSeparator() + "**.json"; + + String[] expected = { + "sub/b.json", "sub/deep/c.json", "sub/deeper/than/foo.json", "test/c/d/foo.json" + }; + + testCollectPathsForGlob(tempDir, relative, expected); + testCollectPathsForGlob(tempDir, absolute, expected); + } + + @Test + void test_collectPathsForGlob_DedupAbsoluteRelative(@TempDir Path tempDir) throws IOException { + + String relative = "**.json"; + String absolute = tempDir.toString() + FileSystems.getDefault().getSeparator() + "**.json"; + + String[] expected = { + "sub/b.json", "sub/deep/c.json", "sub/deeper/than/foo.json", "test/c/d/foo.json" + }; + + testCollectPathsForGlob(tempDir, "{" + absolute + "," + relative + "}", expected); } @Test @@ -145,6 +203,180 @@ void dontPropagateNonRecursiveValuePool() { assertThat(0.9).isEqualTo(valuePools.extractFirstProbability(propagatedType)); } + @Test + void testExtractRawValues_Files_NonRecursive(@TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + String glob = tempDir + "/*.txt"; + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + withValuePoolImplementation(new String[] {}, 1.0, RECURSIVE, glob)); + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements).containsExactly("a.txt", "c.zip.txt"); + } + + @Test + void testExtractRawValues_Files_Recursive(@TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + String glob = tempDir + "/**/*.txt"; + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + withValuePoolImplementation(new String[] {}, 1.0, RECURSIVE, glob)); + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + @Test + void testExtractRawValues_Files_OverlappingPatternsAreDeduped(@TempDir Path tempDir) + throws IOException { + mockSourceDirectory(tempDir); + + String recursiveGlob = tempDir + "/**.txt"; + String directGlob = tempDir + "/*.txt"; + String relativeRecursiveGlob = "**.txt"; + + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + withValuePoolImplementation( + new String[] {}, 1.0, RECURSIVE, recursiveGlob, directGlob, relativeRecursiveGlob)); + + ValuePoolRegistry valuePools; + try { + valuePools = + new ValuePoolRegistry(ValuePoolsTest.class.getMethod("dummyFuzzTestMethod"), tempDir); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + "a.txt", + "c.zip.txt", + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + @Test + void testExtractRawValues_Files_GlobsWithMethodSources(@TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + + String recursiveGlob = tempDir + "/**.txt"; + String directGlob = tempDir + "/*.txt"; + String relativeRecursiveGlob = "**.txt"; + + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + withValuePoolImplementation( + new String[] {"myPool"}, + 1.0, + RECURSIVE, + recursiveGlob, + directGlob, + relativeRecursiveGlob)); + + ValuePoolRegistry valuePools; + try { + valuePools = + new ValuePoolRegistry(ValuePoolsTest.class.getMethod("dummyFuzzTestMethod"), tempDir); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + List elements = + valuePools + .extractUserValues(type) + // if byte[], convert to string + .map( + value -> { + if (value instanceof byte[]) { + return new String((byte[]) value, StandardCharsets.UTF_8); + } else { + return value; + } + }) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + // method sources + "value1", + "value2", + "value3", + // globs + "a.txt", + "c.zip.txt", + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + @Test + void testExtractRawValues_Files_OverlappingAbsoluteRelativePatternsAreDeduped( + @TempDir Path tempDir) throws IOException { + mockSourceDirectory(tempDir); + + String recursiveGlob = tempDir + "/**.txt"; + String directGlob = "**.txt"; + + AnnotatedType type = + withExtraAnnotations( + new TypeHolder() {}.annotatedType(), + withValuePoolImplementation( + new String[] {}, 1.0, RECURSIVE, recursiveGlob, directGlob)); + + List elements = + valuePools + .extractUserValues(type) + .map(value -> new String((byte[]) value, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + assertThat(elements) + .containsExactly( + "a.txt", + "c.zip.txt", + "sub/b.txt", + "sub/deep/c.txt", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "test/c/d/bar.txt"); + } + + private static void writeUtf8(Path path, String content) throws IOException { + Files.write(path, content.getBytes(StandardCharsets.UTF_8)); + } + private static ValuePool[] getValuePoolAnnotations(AnnotatedType type) { return Arrays.stream(type.getAnnotations()) .filter(annotation -> annotation instanceof ValuePool) @@ -159,13 +391,19 @@ public static ValuePool withValuePoolImplementation(String[] value, double p) { return withValuePoolImplementation(value, p, RECURSIVE); } - public static ValuePool withValuePoolImplementation(String[] value, double p, String constraint) { + public static ValuePool withValuePoolImplementation( + String[] value, double p, String constraint, String... files) { return new ValuePool() { @Override public String[] value() { return value; } + @Override + public String[] files() { + return files; + } + @Override public double p() { return p; @@ -188,6 +426,7 @@ public boolean equals(Object o) { } ValuePool other = (ValuePool) o; return Arrays.equals(this.value(), other.value()) + && Arrays.equals(this.files(), other.files()) && this.p() == other.p() && this.constraint().equals(other.constraint()); } @@ -196,8 +435,9 @@ public boolean equals(Object o) { public int hashCode() { int hash = 0; hash += Arrays.hashCode(value()) * 127; - hash += Double.hashCode(p()) * 31 * 127; - hash += constraint().hashCode() * 127; + hash += Arrays.hashCode(files()) * 31 * 127; + hash += Double.hashCode(p()) * 31 * 31 * 127; + hash += constraint().hashCode() * 31 * 31 * 31 * 127; return hash; } @@ -207,6 +447,8 @@ public String toString() { + ValuePool.class.getName() + "(value={" + String.join(", ", value()) + + "}, files={" + + String.join(", ", files()) + "}, p=" + p() + ", constraint=" @@ -215,4 +457,51 @@ public String toString() { } }; } + + void testCollectPathsForGlob(Path tempDir, String glob, String... expected) throws IOException { + mockSourceDirectory(tempDir); + List matchedPaths = + ValuePoolRegistry.collectPathsForGlob(tempDir, glob) + .map(Path::toAbsolutePath) + .collect(Collectors.toList()); + + List expectedPaths = + Arrays.stream(expected) + .map(tempDir::resolve) + .map(Path::toAbsolutePath) + .collect(Collectors.toList()); + + assertThat(matchedPaths).containsExactlyElementsIn(expectedPaths); + } + + private void makeFiles(Path base, String... paths) throws IOException { + for (String path : paths) { + Path file = base.resolve(path); + Files.createDirectories(file.getParent()); + writeUtf8(file, path); + } + } + + private void mockSourceDirectory(Path base) throws IOException { + makeFiles( + base, + // top level + "a.txt", + "b.zip", + "c.zip.txt", + "sub/b.txt", + "sub/b.json", + "sub/b.xml", + "sub/c.zip", + "sub/deep/c.txt", + "sub/deep/c.json", + "sub/deep/c.xml", + "sub/deep/corpus/d.xml", + "sub/deep/corpus/d.txt", + "sub/deeper/than/mah.txt", + "sub/deeper/than/foo.json", + "sub/deeper/than/bar.xml", + "test/c/d/foo.json", + "test/c/d/bar.txt"); + } } From 4f5d213e12489ba94d56d540e2a2fc6729613127 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 16:26:17 +0100 Subject: [PATCH 3/9] chore: build ValuePool using a builder in tests The number of fields in ValuePool is getting large, so that construction using positional args is becoming confusing. Using a builder simplifies the tests. --- .../mutation/support/ValuePoolsTest.java | 188 +++++++++++------- 1 file changed, 112 insertions(+), 76 deletions(-) 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 index 2bc204946..c5209d3c8 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java @@ -20,7 +20,6 @@ import static com.code_intelligence.jazzer.mutation.support.TypeSupport.parameterTypeIfParameterized; import static com.code_intelligence.jazzer.mutation.support.TypeSupport.withExtraAnnotations; import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.DECLARATION; -import static com.code_intelligence.jazzer.mutation.utils.PropertyConstraint.RECURSIVE; import static com.google.common.truth.Truth.assertThat; import com.code_intelligence.jazzer.mutation.annotation.ValuePool; @@ -81,7 +80,7 @@ void testExtractFirstProbability_TwoWithLastUsed() { AnnotatedType type = withExtraAnnotations( new TypeHolder<@ValuePool(value = "myPool", p = 0.2) String>() {}.annotatedType(), - withValuePoolImplementation(new String[] {"myPool2"}, 0.3)); + new ValuePoolBuilder().value("myPool2").p(0.3).build()); double p = valuePools.extractFirstProbability(type); assertThat(p).isEqualTo(0.2); } @@ -117,7 +116,7 @@ void testExtractRawValues_JoinFromTwoSeparateAnnotations() { AnnotatedType type = withExtraAnnotations( new TypeHolder<@ValuePool("myPool2") String>() {}.annotatedType(), - withValuePoolImplementation(new String[] {"myPool"}, 5)); + new ValuePoolBuilder().value("myPool").build()); List elements = valuePools.extractUserValues(type).collect(Collectors.toList()); assertThat(elements).isNotEmpty(); assertThat(elements).containsExactly("value1", "value2", "value3", "value4"); @@ -210,7 +209,7 @@ void testExtractRawValues_Files_NonRecursive(@TempDir Path tempDir) throws IOExc AnnotatedType type = withExtraAnnotations( new TypeHolder() {}.annotatedType(), - withValuePoolImplementation(new String[] {}, 1.0, RECURSIVE, glob)); + new ValuePoolBuilder().files(glob).build()); List elements = valuePools @@ -228,7 +227,7 @@ void testExtractRawValues_Files_Recursive(@TempDir Path tempDir) throws IOExcept AnnotatedType type = withExtraAnnotations( new TypeHolder() {}.annotatedType(), - withValuePoolImplementation(new String[] {}, 1.0, RECURSIVE, glob)); + new ValuePoolBuilder().files(glob).build()); List elements = valuePools @@ -257,8 +256,7 @@ void testExtractRawValues_Files_OverlappingPatternsAreDeduped(@TempDir Path temp AnnotatedType type = withExtraAnnotations( new TypeHolder() {}.annotatedType(), - withValuePoolImplementation( - new String[] {}, 1.0, RECURSIVE, recursiveGlob, directGlob, relativeRecursiveGlob)); + new ValuePoolBuilder().files(recursiveGlob, directGlob, relativeRecursiveGlob).build()); ValuePoolRegistry valuePools; try { @@ -296,13 +294,10 @@ void testExtractRawValues_Files_GlobsWithMethodSources(@TempDir Path tempDir) th AnnotatedType type = withExtraAnnotations( new TypeHolder() {}.annotatedType(), - withValuePoolImplementation( - new String[] {"myPool"}, - 1.0, - RECURSIVE, - recursiveGlob, - directGlob, - relativeRecursiveGlob)); + new ValuePoolBuilder() + .value("myPool") + .files(recursiveGlob, directGlob, relativeRecursiveGlob) + .build()); ValuePoolRegistry valuePools; try { @@ -353,8 +348,7 @@ void testExtractRawValues_Files_OverlappingAbsoluteRelativePatternsAreDeduped( AnnotatedType type = withExtraAnnotations( new TypeHolder() {}.annotatedType(), - withValuePoolImplementation( - new String[] {}, 1.0, RECURSIVE, recursiveGlob, directGlob)); + new ValuePoolBuilder().files(recursiveGlob, directGlob).build()); List elements = valuePools @@ -387,75 +381,117 @@ private static Stream extractValuesFromValuePools(AnnotatedType type) { return Arrays.stream(getValuePoolAnnotations(type)).flatMap(v -> Arrays.stream(v.value())); } - public static ValuePool withValuePoolImplementation(String[] value, double p) { - return withValuePoolImplementation(value, p, RECURSIVE); - } - - public static ValuePool withValuePoolImplementation( - String[] value, double p, String constraint, String... files) { - return new ValuePool() { - @Override - public String[] value() { - return value; + private static class ValuePoolBuilder { + private String[] value; + private String[] files; + private double p; + private String constraint; + + public ValuePoolBuilder() { + try { + value = (String[]) getDefault("value"); + files = (String[]) getDefault("files"); + p = (double) getDefault("p"); + constraint = (String) getDefault("constraint"); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Could not load ValuePool defaults", e); } + } - @Override - public String[] files() { - return files; - } + private Object getDefault(String methodName) throws NoSuchMethodException { + return ValuePool.class.getDeclaredMethod(methodName).getDefaultValue(); + } - @Override - public double p() { - return p; - } + public ValuePoolBuilder value(String... values) { + this.value = values; + return this; + } - @Override - public String constraint() { - return constraint; - } + public ValuePoolBuilder p(double p) { + this.p = p; + return this; + } - @Override - public Class annotationType() { - return ValuePool.class; - } + public ValuePoolBuilder constraint(String constraint) { + this.constraint = constraint; + return this; + } + + public ValuePoolBuilder files(String... files) { + this.files = files; + return this; + } + + public ValuePool build() { + final String[] value = this.value; + final String[] files = this.files; + final double p = this.p; + final String constraint = this.constraint; - @Override - public boolean equals(Object o) { - if (!(o instanceof ValuePool)) { - return false; + return new ValuePool() { + @Override + public Class annotationType() { + return ValuePool.class; } - ValuePool other = (ValuePool) o; - return Arrays.equals(this.value(), other.value()) - && Arrays.equals(this.files(), other.files()) - && this.p() == other.p() - && this.constraint().equals(other.constraint()); - } - @Override - public int hashCode() { - int hash = 0; - hash += Arrays.hashCode(value()) * 127; - hash += Arrays.hashCode(files()) * 31 * 127; - hash += Double.hashCode(p()) * 31 * 31 * 127; - hash += constraint().hashCode() * 31 * 31 * 31 * 127; - return hash; - } + @Override + public String[] value() { + return value; + } - @Override - public String toString() { - return "@" - + ValuePool.class.getName() - + "(value={" - + String.join(", ", value()) - + "}, files={" - + String.join(", ", files()) - + "}, p=" - + p() - + ", constraint=" - + constraint() - + ")"; - } - }; + @Override + public String[] files() { + return files; + } + + @Override + public double p() { + return p; + } + + @Override + public String constraint() { + return constraint; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ValuePool)) { + return false; + } + ValuePool other = (ValuePool) o; + return Arrays.equals(this.value(), other.value()) + && Arrays.equals(this.files(), other.files()) + && this.p() == other.p() + && this.constraint().equals(other.constraint()); + } + + @Override + public int hashCode() { + int hash = 0; + hash += Arrays.hashCode(value()) * 127; + hash += Arrays.hashCode(files()) * 31 * 127; + hash += Double.hashCode(p()) * 31 * 31 * 127; + hash += constraint().hashCode() * 31 * 31 * 31 * 127; + return hash; + } + + @Override + public String toString() { + return "@" + + ValuePool.class.getName() + + "(value={" + + String.join(", ", value()) + + "}, files={" + + String.join(", ", files()) + + "}, p=" + + p() + + ", constraint=" + + constraint() + + ")"; + } + }; + } } void testCollectPathsForGlob(Path tempDir, String glob, String... expected) throws IOException { From d3e2ca54be25c502f8027a6fd5a47fd08542ef77 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 13:26:11 +0100 Subject: [PATCH 4/9] feat: add configurable max number of mutations for ValuePools --- .../jazzer/mutation/annotation/ValuePool.java | 6 ++ .../mutator/lang/ValuePoolMutatorFactory.java | 35 +++++++---- .../mutation/support/ValuePoolRegistry.java | 11 ++++ .../mutation/support/ValuePoolsTest.java | 58 ++++++++++++++++++- 4 files changed, 99 insertions(+), 11 deletions(-) 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 index 4e78ce175..f6be6252c 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/ValuePool.java @@ -101,6 +101,12 @@ */ double p() default 0.1; + /** + * If the mutator selects a value from this {@code ValuePool}, it will perform up to {@code + * maxMutations} additional mutations on the selected value. + */ + int maxMutations() default 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 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 index ddf618639..7c1825dbc 100644 --- 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 @@ -74,12 +74,17 @@ private static final class ValuePoolMutator extends SerializingMutator { private final SerializingMutator mutator; private final List userValues; private final double poolUsageProbability; + private final int maxMutations; ValuePoolMutator( - SerializingMutator mutator, List userValues, double poolUsageProbability) { + SerializingMutator mutator, + List userValues, + double poolUsageProbability, + int maxMutations) { this.mutator = mutator; this.userValues = userValues; this.poolUsageProbability = poolUsageProbability; + this.maxMutations = maxMutations; } @SuppressWarnings("unchecked") @@ -106,7 +111,8 @@ static SerializingMutator wrapIfValuesExist( } double p = valuePoolRegistry.extractFirstProbability(type); - return new ValuePoolMutator<>(mutator, userValues, p); + int maxMutations = valuePoolRegistry.extractFirstMaxMutations(type); + return new ValuePoolMutator<>(mutator, userValues, p, maxMutations); } /** @@ -138,8 +144,8 @@ private static boolean isSerializationStable(SerializingMutator mutator, @Override public String toDebugString(Predicate isInCycle) { return String.format( - "%s (values: %d p: %.2f)", - mutator.toDebugString(isInCycle), userValues.size(), poolUsageProbability); + "%s (values: %d p: %.2f, maxMutations: %d)", + mutator.toDebugString(isInCycle), userValues.size(), poolUsageProbability, maxMutations); } @Override @@ -174,19 +180,28 @@ public T init(PseudoRandom 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); + value = prng.pickIn(userValues); + // Treat the user value as a starting point for mutation + for (int i = 0; i < prng.closedRange(0, maxMutations); i++) { + value = mutator.mutate(value, prng); } + return value; } return mutator.mutate(value, prng); } @Override public T crossOver(T value, T otherValue, PseudoRandom prng) { - return mutator.crossOver(value, otherValue, prng); + if (prng.closedRange(0.0, 1.0) < poolUsageProbability) { + value = prng.pickIn(userValues); + // Treat the user value as a starting point for crossOver + for (int i = 0; i < prng.closedRange(0, maxMutations); i++) { + value = mutator.crossOver(value, prng.pickIn(userValues), prng); + } + return value; + } else { + return mutator.crossOver(value, otherValue, prng); + } } } } 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 index 43f1c0dd0..7f8704c7b 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ValuePoolRegistry.java @@ -87,6 +87,17 @@ public double extractFirstProbability(AnnotatedType type) { return p; } + public int extractFirstMaxMutations(AnnotatedType type) { + ValuePool[] valuePoolAnnotations = type.getAnnotationsByType(ValuePool.class); + if (valuePoolAnnotations.length == 0) { + // If we are here, it's a bug in the caller. + throw new IllegalStateException("Expected to find @ValuePool, but found none."); + } + int maxMutations = valuePoolAnnotations[0].maxMutations(); + require(maxMutations >= 0, "@ValuePool maxMutations must be >= 0, but was " + maxMutations); + return maxMutations; + } + public Stream extractUserValues(AnnotatedType type) { Stream valuesFromSourceMethods = getValuePoolAnnotations(type).stream() 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 index c5209d3c8..c28651baa 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ValuePoolsTest.java @@ -85,6 +85,46 @@ void testExtractFirstProbability_TwoWithLastUsed() { assertThat(p).isEqualTo(0.2); } + @Test + void testExtractFirstMaxMutations_Default() { + AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); + int maxMutations = valuePools.extractFirstMaxMutations(type); + assertThat(maxMutations).isEqualTo(1); + } + + @Test + void testExtractFirstMaxMutations_OneUserDefined() { + AnnotatedType type = + new TypeHolder< + @ValuePool(value = "myPool2", maxMutations = 10) String>() {}.annotatedType(); + int maxMutations = valuePools.extractFirstMaxMutations(type); + assertThat(maxMutations).isEqualTo(10); + } + + @Test + void testExtractMaxMutations_TwoWithLastUsed() { + AnnotatedType type = + withExtraAnnotations( + new TypeHolder< + @ValuePool(value = "myPool", maxMutations = 2) String>() {}.annotatedType(), + new ValuePoolBuilder().value("myPool2").maxMutations(10).build()); + int maxMutations = valuePools.extractFirstMaxMutations(type); + assertThat(maxMutations).isEqualTo(2); + } + + // assert that maxMutatiosn throws when negative + @Test + void testExtractFirstMaxMutations_Negative() { + AnnotatedType type = + new TypeHolder< + @ValuePool(value = "myPool2", maxMutations = -1) String>() {}.annotatedType(); + try { + valuePools.extractFirstMaxMutations(type); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("@ValuePool maxMutations must be >= 0"); + } + } + @Test void testExtractRawValues_OneAnnotation() { AnnotatedType type = new TypeHolder<@ValuePool("myPool") String>() {}.annotatedType(); @@ -385,6 +425,7 @@ private static class ValuePoolBuilder { private String[] value; private String[] files; private double p; + private int maxMutations; private String constraint; public ValuePoolBuilder() { @@ -392,6 +433,7 @@ public ValuePoolBuilder() { value = (String[]) getDefault("value"); files = (String[]) getDefault("files"); p = (double) getDefault("p"); + maxMutations = (int) getDefault("maxMutations"); constraint = (String) getDefault("constraint"); } catch (NoSuchMethodException e) { throw new RuntimeException("Could not load ValuePool defaults", e); @@ -412,6 +454,11 @@ public ValuePoolBuilder p(double p) { return this; } + public ValuePoolBuilder maxMutations(int maxMutations) { + this.maxMutations = maxMutations; + return this; + } + public ValuePoolBuilder constraint(String constraint) { this.constraint = constraint; return this; @@ -426,6 +473,7 @@ public ValuePool build() { final String[] value = this.value; final String[] files = this.files; final double p = this.p; + final int maxMutations = this.maxMutations; final String constraint = this.constraint; return new ValuePool() { @@ -449,6 +497,11 @@ public double p() { return p; } + @Override + public int maxMutations() { + return maxMutations; + } + @Override public String constraint() { return constraint; @@ -472,7 +525,8 @@ public int hashCode() { hash += Arrays.hashCode(value()) * 127; hash += Arrays.hashCode(files()) * 31 * 127; hash += Double.hashCode(p()) * 31 * 31 * 127; - hash += constraint().hashCode() * 31 * 31 * 31 * 127; + hash += Integer.hashCode(maxMutations()) * 31 * 31 * 31 * 127; + hash += constraint().hashCode() * 31 * 31 * 31 * 31 * 127; return hash; } @@ -486,6 +540,8 @@ public String toString() { + String.join(", ", files()) + "}, p=" + p() + + ", maxMutations=" + + maxMutations() + ", constraint=" + constraint() + ")"; From c656026c0993ff28bbf8bebe8899b6afaed95ede Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 13:03:31 +0100 Subject: [PATCH 5/9] feat: document @ValuePool --- docs/mutation-framework.md | 157 +++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/docs/mutation-framework.md b/docs/mutation-framework.md index 59c6e4c58..138a03bef 100644 --- a/docs/mutation-framework.md +++ b/docs/mutation-framework.md @@ -317,6 +317,163 @@ class SimpleClassFuzzTests { } ``` +## @ValuePool: Guide fuzzing with custom values + +The `@ValuePool` annotation lets you provide concrete example values that Jazzer's mutators will use when generating test inputs. +This helps guide fuzzing toward realistic or edge-case values relevant to your application. + +### Basic Usage + +You can apply `@ValuePool` in two places: +- **On method parameters** - values apply only to that parameter's type +- **On the test method itself** - values propagate to all matching types across all parameters + +**Example:** +```java +@FuzzTest +void fuzzTest(@ValuePool(value = {"mySupplier"}) Map foo) { + // Strings from mySupplier feed the Map's String mutator + // Integers from mySupplier feed the Map's Integer mutator +} + +@FuzzTest +@ValuePool(value = {"mySupplier"}) +void anotherFuzzTest(Map foo, String bar) { + // Values propagate to ALL matching types: + // - String mutator for Map keys in 'foo' + // - String mutator for 'bar' + // - Integer mutator for Map values in 'foo' +} + +static Stream mySupplier() { + return Stream.of("example1", "example2", "example3", 1232187321, -182371); +} +``` + +### How Type Matching Works + +Jazzer automatically routes values to mutators based on type: +- Strings in your value pool → String mutators +- Integers in your value pool → Integer mutators +- Byte arrays in your value pool → byte[] mutators + +**Type propagation happens recursively by default**, so a `@ValuePool` on a `Map` will feed both the String mutator (for keys) and Integer mutator (for values). + +--- + +### Supplying Values: Two Mechanisms + +#### 1. Supplier Methods (`value` field) + +Provide the names of static methods that return `Stream`: +```java +@ValuePool(value = {"mySupplier", "anotherSupplier"}) +``` + +**Requirements:** +- Methods must be `static` +- Must return `Stream` +- Can contain mixed types (Jazzer routes by type automatically) + +#### 2. File Patterns (`files` field) + +Load files as `byte[]` arrays using glob patterns: +```java +@ValuePool(files = {"*.jpeg"}) // All JPEGs in working dir +@ValuePool(files = {"**.xml"}) // All XMLs recursively +@ValuePool(files = {"/absolute/path/**"}) // All files from absolute path +@ValuePool(files = {"*.jpg", "**.png"}) // Multiple patterns +``` + +**Glob syntax:** Follows `java.nio.file.PathMatcher` with `glob:` pattern rules. + +**You can combine both mechanisms:** +```java +@ValuePool(value = {"mySupplier"}, files = {"test-data/*.json"}) +``` + +--- + +### Configuration Options + +#### Mutation Probability (`p` field) +Controls how often values from the pool are used versus randomly generated values. +```java +@ValuePool(value = {"mySupplier"}, p = 0.3) // Use pool values 30% of the time +``` + +**Default:** `p = 0.1` (10% of mutations use pool values) +**Range:** 0.0 to 1.0 + +#### Type Propagation (`constraint` field) + +Controls whether the annotation affects nested types: +```java +// Default: RECURSIVE - applies to all nested types +@ValuePool(value = {"mySupplier"}, constraint = Constraint.RECURSIVE) + +// DECLARATION - applies only to the annotated type, not subtypes +@ValuePool(value = {"mySupplier"}, constraint = Constraint.DECLARATION) +``` + +**Example of the difference:** +```java +// With RECURSIVE (default): +@ValuePool(value = {"stringSupplier"}) Map> data +// Strings feed both Map keys AND List elements + +// With DECLARATION: +@ValuePool(value = {"stringSupplier"}, constraint = DECLARATION) Map> data +// Strings only feed Map keys, NOT List elements +``` + +--- + +### Complete Example +```java +class MyFuzzTest { + static Stream edgeCases() { + return Stream.of( + "", "null", "alert('xss')", // Strings + 0, -1, Integer.MAX_VALUE, // Integers + new byte[]{0x00, 0xFF} // A byte array + ); + } + + @FuzzTest + @ValuePool( + value = {"edgeCases"}, + files = {"test-inputs/*.bin"}, + p = 0.25 // Use pool values 25% of the time + ) + void testParser(String input, Map config, byte[] data) { + // All three parameters get values from the pool: + // - 'input' gets Strings + // - 'config' keys get Strings, values get Integers + // - 'data' gets bytes from both edgeCases() and *.bin files + } +} +``` + +--- + +#### Max Mutations (`maxMutations` field) + +After selecting a value from the pool, the mutator can apply additional random mutations to it. +```java +@ValuePool(value = {"mySupplier"}, maxMutations = 5) +``` + +**Default:** `maxMutations = 1` (one additional mutation applied) +**Range:** 0 or higher + +**How it works:** If `maxMutations = 5`, Jazzer will: +1. Select a value from your pool (e.g., `"example"`) +2. Apply up to 5 random mutations (e.g., `"example"` → `"exAmple"` → `"exAmple123"` → ...) + +This helps explore variations of your seed values while staying close to realistic inputs. + + ## FuzzedDataProvider The `FuzzedDataProvider` is an alternative approach commonly used in programming From 96056469b64ed33a30f667c7fb5ec0d44c9e5277 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 20:01:17 +0100 Subject: [PATCH 6/9] chore: increase test timeout for the selffuzz test --- .../java/com/code_intelligence/selffuzz/mutation/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel index 46b209ef2..7dc25be62 100644 --- a/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel +++ b/selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/BUILD.bazel @@ -2,6 +2,7 @@ load("//bazel:fuzz_target.bzl", "java_fuzz_target_test") java_fuzz_target_test( name = "ArgumentsMutatorFuzzTest", + timeout = "long", srcs = [ "ArgumentsMutatorFuzzTest.java", "BeanWithParent.java", From e0aa70877219b3f857ef7ae802db146e9249edb0 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 18:09:38 +0100 Subject: [PATCH 7/9] TMP: fix: defensive stack dump - check of JVM is still alive before dumping the stack --- .../jazzer/driver/fuzz_target_runner.cpp | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp b/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp index cdcfc3082..edea9b129 100644 --- a/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp +++ b/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp @@ -114,20 +114,45 @@ extern "C" size_t LLVMFuzzerCustomCrossOver(const uint8_t *Data1, size_t Size1, namespace jazzer { void DumpJvmStackTraces() { + // Best-effort diagnostic during crash/death - may itself crash if JVM is + // shutting down + if (gJavaVm == nullptr || gRunner == nullptr) { + return; + } + JNIEnv *env = nullptr; - if (gJavaVm->AttachCurrentThread(reinterpret_cast(&env), nullptr) != - JNI_OK) { + jint attach_result = + gJavaVm->AttachCurrentThread(reinterpret_cast(&env), nullptr); + if (attach_result != JNI_OK || env == nullptr) { + return; + } + + // gRunner may have been cleared already during JVM shutdown. + if (env->IsSameObject(gRunner, nullptr)) { return; } + jmethodID dumpStack = env->GetStaticMethodID(gRunner, "dumpAllStackTraces", "()V"); + if (dumpStack == nullptr) { + // Silently clear - we're in a death callback, diagnostics may not work + if (env->ExceptionCheck()) { + env->ExceptionClear(); + } + return; + } + if (env->ExceptionCheck()) { + // This might crash during JVM shutdown, but worth trying for diagnostics env->ExceptionDescribe(); + env->ExceptionClear(); return; } env->CallStaticVoidMethod(gRunner, dumpStack); if (env->ExceptionCheck()) { + // This might crash during JVM shutdown, but worth trying for diagnostics env->ExceptionDescribe(); + env->ExceptionClear(); return; } // Do not detach as we may be the main thread (but the JVM exits anyway). From a53e9fa3bd7cad6fff1c8c37514613bb1fe14d61 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 20:26:39 +0100 Subject: [PATCH 8/9] TMP: revert to old and rerun tests --- .../jazzer/driver/fuzz_target_runner.cpp | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp b/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp index edea9b129..8049b1668 100644 --- a/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp +++ b/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp @@ -113,50 +113,71 @@ extern "C" size_t LLVMFuzzerCustomCrossOver(const uint8_t *Data1, size_t Size1, } namespace jazzer { -void DumpJvmStackTraces() { - // Best-effort diagnostic during crash/death - may itself crash if JVM is - // shutting down - if (gJavaVm == nullptr || gRunner == nullptr) { - return; - } +void DumpJvmStackTraces() { JNIEnv *env = nullptr; - jint attach_result = - gJavaVm->AttachCurrentThread(reinterpret_cast(&env), nullptr); - if (attach_result != JNI_OK || env == nullptr) { + if (gJavaVm->AttachCurrentThread(reinterpret_cast(&env), nullptr) != + JNI_OK) { return; } - - // gRunner may have been cleared already during JVM shutdown. - if (env->IsSameObject(gRunner, nullptr)) { - return; - } - jmethodID dumpStack = env->GetStaticMethodID(gRunner, "dumpAllStackTraces", "()V"); - if (dumpStack == nullptr) { - // Silently clear - we're in a death callback, diagnostics may not work - if (env->ExceptionCheck()) { - env->ExceptionClear(); - } - return; - } - if (env->ExceptionCheck()) { - // This might crash during JVM shutdown, but worth trying for diagnostics env->ExceptionDescribe(); - env->ExceptionClear(); return; } env->CallStaticVoidMethod(gRunner, dumpStack); if (env->ExceptionCheck()) { - // This might crash during JVM shutdown, but worth trying for diagnostics env->ExceptionDescribe(); - env->ExceptionClear(); return; } // Do not detach as we may be the main thread (but the JVM exits anyway). } + +//void DumpJvmStackTraces() { +// // Best-effort diagnostic during crash/death - may itself crash if JVM is +// // shutting down +// if (gJavaVm == nullptr || gRunner == nullptr) { +// return; +// } +// +// JNIEnv *env = nullptr; +// jint attach_result = +// gJavaVm->AttachCurrentThread(reinterpret_cast(&env), nullptr); +// if (attach_result != JNI_OK || env == nullptr) { +// return; +// } +// +// // gRunner may have been cleared already during JVM shutdown. +// if (env->IsSameObject(gRunner, nullptr)) { +// return; +// } +// +// jmethodID dumpStack = +// env->GetStaticMethodID(gRunner, "dumpAllStackTraces", "()V"); +// if (dumpStack == nullptr) { +// // Silently clear - we're in a death callback, diagnostics may not work +// if (env->ExceptionCheck()) { +// env->ExceptionClear(); +// } +// return; +// } +// +// if (env->ExceptionCheck()) { +// // This might crash during JVM shutdown, but worth trying for diagnostics +// env->ExceptionDescribe(); +// env->ExceptionClear(); +// return; +// } +// env->CallStaticVoidMethod(gRunner, dumpStack); +// if (env->ExceptionCheck()) { +// // This might crash during JVM shutdown, but worth trying for diagnostics +// env->ExceptionDescribe(); +// env->ExceptionClear(); +// return; +// } +// // Do not detach as we may be the main thread (but the JVM exits anyway). +//} } // namespace jazzer [[maybe_unused]] jint From 6d874baf70bea2beed8774bfeb64075f7ff6a895 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Wed, 21 Jan 2026 20:04:18 +0100 Subject: [PATCH 9/9] TMP: upload the corpus to the artifacts for local reproduction --- .github/workflows/run-all-tests-pr.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests-pr.yml b/.github/workflows/run-all-tests-pr.yml index e94248490..06244a027 100644 --- a/.github/workflows/run-all-tests-pr.yml +++ b/.github/workflows/run-all-tests-pr.yml @@ -70,7 +70,9 @@ jobs: - name: Copy Bazel log if: always() shell: bash - run: cp "$(readlink bazel-out)"/../../../java.log* . + run: | + cp "$(readlink bazel-out)"/../../../java.log* . + cp -r selffuzz/src/test/resources/.corpus ./corpus_backup - name: Upload test logs if: always() @@ -81,3 +83,4 @@ jobs: path: | bazel-testlogs*/**/test.log java.log* + corpus_backup/**