Jazzer: Coverage-Guided Java/JVM Fuzzing in CI Pipelines
Java has a reputation for safety — garbage collection eliminates entire classes of memory bugs, strong typing prevents many type errors, and the JVM sandboxes execution. But Java applications can still have serious bugs: path traversal vulnerabilities in file handling, deserialization exploits, XML/JSON parsing crashes, SQL injection through improperly sanitized inputs, and business logic errors that manifest only with specific data combinations. Jazzer brings coverage-guided fuzzing to the JVM, finding these bugs automatically with the same approach that libFuzzer uses for C/C++.
What Jazzer Does
Jazzer is an open-source JVM fuzzer maintained by Code Intelligence. It:
- Runs on the JVM and instruments Java/Kotlin/Scala bytecode for coverage tracking
- Uses libFuzzer's mutation engine under the hood for coverage-guided mutation
- Integrates with JUnit 5 via
@FuzzTestannotations, making fuzz tests look like regular unit tests - Supports structured fuzzing with
FuzzedDataProvider(similar to Atheris) - Detects crashes, uncaught exceptions, and common vulnerability patterns automatically
The key differentiator from other Java testing tools is coverage guidance: Jazzer instruments your bytecode and tells libFuzzer which branches each input exercises, allowing the mutation engine to actively seek unexplored code paths rather than generating random inputs.
Installation
Maven:
<dependency>
<groupId>com.code-intelligence</groupId>
<artifactId>jazzer-junit</artifactId>
<version>0.22.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.code-intelligence</groupId>
<artifactId>jazzer-api</artifactId>
<version>0.22.1</version>
<scope>test</scope>
</dependency>Gradle (Kotlin DSL):
dependencies {
testImplementation("com.code-intelligence:jazzer-junit:0.22.1")
testImplementation("com.code-intelligence:jazzer-api:0.22.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
}
tasks.test {
useJUnitPlatform()
}Writing Your First Fuzz Test
The JUnit 5 integration makes fuzz tests look like regular unit tests:
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import org.junit.jupiter.api.Test;
import com.example.JsonParser;
import com.example.ParseException;
class JsonParserFuzzTest {
// Regular unit test — runs normally in CI
@Test
void testValidJson() {
JsonParser parser = new JsonParser();
var result = parser.parse("{\"key\": \"value\"}");
assertEquals("value", result.getString("key"));
}
// Fuzz test — runs with Jazzer when fuzzing is enabled
@FuzzTest
void fuzzJsonParser(FuzzedDataProvider data) {
String input = data.consumeString(1000);
JsonParser parser = new JsonParser();
try {
parser.parse(input);
} catch (ParseException e) {
// Expected — invalid input should throw ParseException
}
// Any other uncaught exception = bug
// Jazzer reports it automatically
}
}The @FuzzTest annotation is the key: in normal test runs (mvn test), Jazzer runs the fuzz test a small number of times with fixed seeds for regression testing. In fuzzing mode, Jazzer runs it continuously with libFuzzer mutation.
FuzzedDataProvider for Structured Inputs
Real-world functions take structured inputs, not raw bytes. Use FuzzedDataProvider to extract typed values:
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
class UserServiceFuzzTest {
@FuzzTest
void fuzzCreateUser(FuzzedDataProvider data) {
String username = data.consumeString(50);
String email = data.consumeString(100);
int age = data.consumeInt();
boolean isAdmin = data.consumeBoolean();
// Pick from a list of valid roles
String role = data.pickValue(new String[]{"USER", "MODERATOR", "ADMIN"});
UserService service = new UserService(new InMemoryUserRepository());
try {
User user = service.createUser(
new CreateUserRequest(username, email, age, isAdmin, role)
);
// Invariant: created user should be findable
assertNotNull(service.findById(user.getId()));
} catch (ValidationException e) {
// Expected for invalid input
}
}
}Available FuzzedDataProvider methods for Java:
data.consumeBoolean()
data.consumeByte()
data.consumeShort()
data.consumeInt()
data.consumeLong()
data.consumeFloat()
data.consumeDouble()
data.consumeString(maxLength)
data.consumeAsciiString(maxLength)
data.consumeBytes(maxLength) // byte[]
data.consumeRemainingAsString()
data.consumeRemainingAsBytes()
data.pickValue(T[] choices) // random element from array
data.pickValue(List<T> choices)
data.remainingBytes()Detecting Common Java Vulnerabilities
Jazzer has built-in detectors for common vulnerability patterns:
Path traversal:
@FuzzTest
void fuzzFileRead(FuzzedDataProvider data) {
String filename = data.consumeString(200);
// Jazzer detects if filename contains "../" and the code reads the file
// This catches path traversal vulnerabilities automatically
try {
fileService.readUserFile(filename);
} catch (IllegalArgumentException | IOException e) {
// Expected if filename is invalid
}
}Deserialization:
@FuzzTest
void fuzzDeserialization(FuzzedDataProvider data) {
byte[] serializedData = data.consumeRemainingAsBytes();
// Jazzer has built-in detection for unsafe deserialization gadget chains
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(serializedData))) {
ois.readObject();
} catch (ClassNotFoundException | IOException e) {
// Expected
}
}SQL injection via JDBC:
@FuzzTest
void fuzzUserSearch(FuzzedDataProvider data) {
String searchQuery = data.consumeString(100);
// Jazzer detects SQL injection if the query is used in a PreparedStatement incorrectly
try {
List<User> results = userRepository.searchByName(searchQuery);
} catch (Exception e) {
// Expected for invalid queries
}
}Fuzzing a Spring Boot API
For Spring Boot applications, fuzz the service layer directly (not through HTTP):
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class OrderServiceFuzzTest {
@Autowired
private OrderService orderService;
@Autowired
private ProductRepository productRepository;
@FuzzTest
void fuzzCreateOrder(FuzzedDataProvider data) {
// Generate a random order request
int productId = data.consumeIntInRange(1, 10000);
int quantity = data.consumeIntInRange(0, Integer.MAX_VALUE);
String couponCode = data.consumeAsciiString(20);
String shippingAddress = data.consumeString(500);
CreateOrderRequest request = new CreateOrderRequest(
productId, quantity, couponCode, shippingAddress
);
try {
Order order = orderService.createOrder("test-user-id", request);
// Invariant: order total should never be negative
assertTrue(order.getTotal().compareTo(BigDecimal.ZERO) >= 0,
"Order total should not be negative: " + order.getTotal());
// Invariant: order should be retrievable
Order retrieved = orderService.getOrder(order.getId());
assertEquals(order.getId(), retrieved.getId());
} catch (ProductNotFoundException | InsufficientStockException |
InvalidCouponException | ValidationException e) {
// Expected business exceptions
}
}
}Maven/Gradle Integration for CI
Maven — run fuzz tests with a custom target:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<systemPropertyVariables>
<!-- Enable fuzzing mode with 60-second time limit -->
<jazzer.fuzz>true</jazzer.fuzz>
<jazzer.fuzz_duration>60</jazzer.fuzz_duration>
<jazzer.corpus_dir>src/test/resources/fuzzcorpus</jazzer.corpus_dir>
</systemPropertyVariables>
</configuration>
</plugin>Gradle:
tasks.register<Test>("fuzzTest") {
useJUnitPlatform {
includeTags("fuzz")
}
systemProperty("jazzer.fuzz", "true")
systemProperty("jazzer.fuzz_duration", "300")
systemProperty("jazzer.corpus_dir", "src/test/resources/fuzzcorpus")
}Tag your fuzz tests for separate execution:
@Tag("fuzz")
@FuzzTest
void fuzzCreateOrder(FuzzedDataProvider data) { ... }GitHub Actions CI
name: Jazzer Fuzz Tests
on:
schedule:
- cron: '0 2 * * *'
push:
paths:
- 'src/main/**'
workflow_dispatch:
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Restore fuzz corpus
uses: actions/cache@v3
with:
path: src/test/resources/fuzzcorpus
key: fuzz-corpus-${{ hashFiles('src/main/**') }}
restore-keys: fuzz-corpus-
- name: Run Jazzer fuzz tests
run: |
./gradlew fuzzTest \
-Djazzer.fuzz=true \
-Djazzer.fuzz_duration=300 \
--continue
- name: Save corpus
uses: actions/cache/save@v3
if: always()
with:
path: src/test/resources/fuzzcorpus
key: fuzz-corpus-${{ github.sha }}
- name: Upload crash reproducers
uses: actions/upload-artifact@v3
if: failure()
with:
name: jazzer-crashes
path: |
crash-*
timeout-*What Jazzer Finds in Practice
Real bugs found by Jazzer in popular Java libraries:
- Apache Commons Text: Expression Language injection via
StringSubstitutor— CVE-2022-42889 ("Text4Shell"), CVSS 9.8 - snakeyaml: Stack overflow via deeply nested YAML — affecting hundreds of libraries that use snakeyaml
- Netty: Heap buffer overflow in HTTP/2 decoder
- Gson: Stack overflow on deeply nested JSON
- Jackson Databind: Various deserialization issues
The pattern is consistent: parsing and deserialization code that handles deeply nested, unusually large, or malformed inputs is where Java vulnerabilities hide. Jazzer finds them quickly because coverage guidance leads the fuzzer directly into the code paths that handle edge cases in format parsing.
Integrating Jazzer into your Java project takes an afternoon. Running fuzz tests nightly means your most critical security surface is continuously tested with far more input diversity than any manual test suite can provide.