Conversation
8058002 to
4aabeac
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces the @ElementOf annotation to allow restricting generated values to a fixed set of constants during mutation testing. The annotation supports all primitive types (and their boxed equivalents) as well as String.
- Adds
@ElementOfannotation with type-specific array fields (bytes, shorts, integers, longs, chars, floats, doubles, strings) - Implements
ElementOfMutatorFactoryto create mutators that select from the provided value sets - Updates documentation with usage examples and annotation reference table
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/com/code_intelligence/jazzer/mutation/annotation/ElementOf.java | New annotation definition with array fields for each supported type |
| src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ElementOfMutatorFactory.java | Factory implementation that creates mutators for selecting from fixed value sets |
| src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java | Registers the new ElementOfMutatorFactory in the mutation chain |
| src/test/java/com/code_intelligence/jazzer/mutation/mutator/lang/ElementOfMutatorFactoryTest.java | Unit tests verifying integer and string selection behavior, plus validation |
| src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java | Integration test case for string array selection |
| docs/mutation-framework.md | Documentation updates with annotation reference and usage example |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/main/java/com/code_intelligence/jazzer/mutation/annotation/ElementOf.java
Outdated
Show resolved
Hide resolved
b4ddf7a to
64ffa36
Compare
64ffa36 to
8b05445
Compare
There was a problem hiding this comment.
Nice addition, thanks!
It works great if the user never removes @ElementOf annotation from the type.
My main issue is that in the corpus we save indices and not the actual values.
This is especially bad for strings if the user's intention is to help the fuzz test boost coverage, and then delete the @ElementOf annotation.
Doing so instantly splits the coprus into two parts---one that gives you coverage with @ElementOf and one without it. You cannot join them.
It is ok-ish only for integers, that have the same byte size, because the bytes that come after are still correctly aligned.
For the other integers, the rest of the bytes is misaligned and probably worthless.
The order of the elements matters, because the corpus only contains indices.
Inserting an element in-between the element list will invalidate the corpus that contains indices for all elements after.
I think, by saving the actual values in the corpus, instead of just indices, we could make this annotation more efficient for users who tend to frequently tweak their fuzz test signature. The only tricky part is to de/serialize strings, but it will make this annotation a lot better. I have prototyped an implementation that does that, take a look at the branch element-of-copy if you like!
pre-approving this, but you know what to do!
src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/LangMutators.java
Show resolved
Hide resolved
src/main/java/com/code_intelligence/jazzer/mutation/annotation/ElementOf.java
Show resolved
Hide resolved
src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ElementOfMutatorFactory.java
Outdated
Show resolved
Hide resolved
src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ElementOfMutatorFactory.java
Outdated
Show resolved
Hide resolved
src/main/java/com/code_intelligence/jazzer/mutation/mutator/lang/ElementOfMutatorFactory.java
Show resolved
Hide resolved
8b05445 to
8e35e63
Compare
Great review, thanks! |
8e35e63 to
c9c1053
Compare
c9c1053 to
6242a09
Compare
|
@oetr I went with your suggestion to store the serialized elements directly in the corpus instead of the index. Do you want to give it another look? |
This allows picking from a fixed set of values during mutation.
6242a09 to
9150f43
Compare
oetr
left a comment
There was a problem hiding this comment.
The current version has problems with corpus stability, and a bug found by quick fuzzing.
| new ValueSerializer<String>() { | ||
| @Override | ||
| public String read(DataInputStream in) throws IOException { | ||
| return in.readUTF(); |
There was a problem hiding this comment.
This causes an exception thrown quite quickly on this fuzz test:
@SelfFuzzTest
@Solo
void fuzz_ElementsOf(@ElementOf(strings = {"A"}) @NotNull String s ) { }INFO: Instrumented com.code_intelligence.selffuzz.jazzer.mutation.support.InputStreamSupport$1 (took 0 ms, size +0%)
== Java Exception: java.lang.RuntimeException: In method: void com.code_intelligence.selffuzz.mutation.ArgumentsMutatorFuzzTest.fuzz_ElementsOf(java.lang.String)
at com.code_intelligence.selffuzz.mutation.ArgumentsMutatorFuzzTest.allTests(ArgumentsMutatorFuzzTest.java:109)
Caused by: java.io.UncheckedIOException: java.io.UTFDataFormatException: malformed input around byte 0
at com.code_intelligence.selffuzz.jazzer.mutation.ArgumentsMutator.read(ArgumentsMutator.java:159)
at com.code_intelligence.selffuzz.mutation.ArgumentsMutatorFuzzTest.allTests(ArgumentsMutatorFuzzTest.java:99)
Caused by: java.io.UTFDataFormatException: malformed input around byte 0
at java.base/java.io.DataInputStream.readUTF(DataInputStream.java:641)
at java.base/java.io.DataInputStream.readUTF(DataInputStream.java:550)
at com.code_intelligence.selffuzz.jazzer.mutation.mutator.lang.ElementOfMutatorFactory$9.read(ElementOfMutatorFactory.java:302)
at com.code_intelligence.selffuzz.jazzer.mutation.mutator.lang.ElementOfMutatorFactory$9.read(ElementOfMutatorFactory.java:299)
at com.code_intelligence.selffuzz.jazzer.mutation.mutator.lang.ElementOfMutatorFactory$1.read(ElementOfMutatorFactory.java:112)
at com.code_intelligence.selffuzz.jazzer.mutation.api.Serializer.readExclusive(Serializer.java:96)
at com.code_intelligence.selffuzz.jazzer.mutation.combinator.InPlaceProductMutator.readExclusive(InPlaceProductMutator.java:73)
at com.code_intelligence.selffuzz.jazzer.mutation.ArgumentsMutator.read(ArgumentsMutator.java:156)
My LLM-reviewer incidentally found that using DataInputStream.readUTF() violates the second commandment of our Serializer: "Thou MUST NOT throw upon garbage streams":
public interface Serializer<T> extends Detacher<T> {
/**
* Reads a {@code T} from an endless stream that is eventually 0.
*
* <p>Implementations
*
* <ul>
* <li>MUST not attempt to consume the entire stream;
* <li>MUST return a valid {@code T} and not throw for any (even garbage) stream;
* <li>SHOULD short-circuit the creation of nested structures upon reading null bytes.
* </ul>
*
* @param in an endless stream that eventually only reads null bytes
* @return a {@code T} constructed from the bytes read
* @throws IOException declared, but must not be thrown by implementations unless methods called
* on {@code in} do
*/
@CheckReturnValue
T read(DataInputStream in) throws IOException;It also found that using writeUTF violates another commandment and can throw if the string length exceeds 65k in length, though I doubt anybody would use a string so big in ElementOf.
It looks like we should reuse the way our StringMutatorFactory reads data. Write is probably ok.
| new ValueSerializer<String>() { | ||
| @Override | ||
| public String read(DataInputStream in) throws IOException { | ||
| return in.readUTF(); |
There was a problem hiding this comment.
This approach reads and writes strings differently from our StringMutatorFactory, which is under the hood LibFuzzerMutatorFactory, so adding/removing the @ElementOf annotation to a string immediately breaks the corpus.
Other types are ok.
| "@ElementOf %s array must contain at least one value for %s", | ||
| fieldName, targetTypeName)); | ||
| // Build index map for O(1) lookups | ||
| Map<T, Integer> valueToIndex = new HashMap<>(); |
There was a problem hiding this comment.
Nice, using a map as opposed to set allows more efficient mutations.
| @Override | ||
| public T mutate(T value, PseudoRandom prng) { | ||
| Integer currentIndex = valueToIndex.get(value); | ||
| if (currentIndex == null) { |
There was a problem hiding this comment.
Can this ever happen?
If it does, I would expect that we throw an exception, because ElementOf is supposed to be self-contained.
|
|
||
| @Override | ||
| public T crossOver(T value, T otherValue, PseudoRandom prng) { | ||
| return prng.choice() ? value : otherValue; |
There was a problem hiding this comment.
This doesn't check that the values exist in the set as opposed to the mutate function.
My understanding is that it cannot happen, but both functions should do (or not do) the same checks.
| public @interface ElementOf { | ||
| byte[] bytes() default {}; | ||
|
|
||
| short[] shorts() default {}; |
There was a problem hiding this comment.
Multiple entries are allowed, is this what we want?
@ElementOf(strings={"A"}, integers={0}) String sThe doc-string explicitly forbids it: exactly one of the type-specific arrays with at least one value
Allows picking from a fixed set of values during mutation.