Jazzer: Coverage-Guided Java/JVM Fuzzing in CI Pipelines

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 @FuzzTest annotations, 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.

Read more