Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions examples/junit/src/test/java/com/example/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,24 @@ java_fuzz_target_test(
"@maven//:org_junit_jupiter_junit_jupiter_params",
],
)

# Test for the maximize() hill-climbing API.
# This test uses Jazzer.maximize() to guide the fuzzer toward maximizing
# a "temperature" value, demonstrating hill-climbing behavior.
java_fuzz_target_test(
name = "ReactorFuzzTest",
srcs = ["ReactorFuzzTest.java"],
allowed_findings = ["java.lang.RuntimeException"],
env = {"JAZZER_FUZZ": "1"},
target_class = "com.example.ReactorFuzzTest",
verify_crash_reproducer = False,
runtime_deps = [
":junit_runtime",
],
deps = [
"//src/main/java/com/code_intelligence/jazzer/api:hooks",
"//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test",
"//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
"@maven//:org_junit_jupiter_junit_jupiter_api",
],
)
59 changes: 59 additions & 0 deletions examples/junit/src/test/java/com/example/ReactorFuzzTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2026 Code Intelligence GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example;

import com.code_intelligence.jazzer.api.Jazzer;
import com.code_intelligence.jazzer.junit.FuzzTest;
import com.code_intelligence.jazzer.mutation.annotation.NotNull;

public class ReactorFuzzTest {

@FuzzTest
public void fuzz(@NotNull String input) {
for (char c : input.toCharArray()) {
if (c < 32 || c > 126) return;
}
controlReactor(input);
}

private void controlReactor(String commands) {
long temperature = 0; // Starts cold

for (char cmd : commands.toCharArray()) {
// Complex, chaotic feedback loop.
// It is hard to predict which character increases temperature
// because it depends on the CURRENT temperature.
if ((temperature ^ cmd) % 3 == 0) {
temperature += (cmd % 10); // Heat up slightly
} else if ((temperature ^ cmd) % 3 == 1) {
temperature -= (cmd % 8); // Cool down slightly
} else {
temperature += 1; // Tiny increase
}

// Prevent dropping below absolute zero for simulation sanity
if (temperature < 0) temperature = 0;
}
// THE GOAL: MAXIMIZATION
// We need to drive 'temperature' to an extreme value.
// Standard coverage is 100% constant here (it just loops).
Jazzer.maximize(temperature, 500, 4500);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here:

  1. This suggests that users can dynamically set the values minValue and maxValue, whereas we assume them to be constant. I think, we should enforce that by double-checking that the values remain constant each time we call Jazzer.maximize() and error out if they do not.

  2. Also, the exceptions thrown in the hooked method don't stop the fuzzer:

  @FuzzTest(maxExecutions = 10)
  public void test(@InRange(min=10, max=20) int high, boolean dummy) {
    Jazzer.maximize(2, 0, high);
  }

Results in exceptions thrown, but the fuzzing process not stopped. Maybe it's because they are thrown in the hook?

INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: seed corpus: files: 1 min: 5b max: 5b total: 5b rss: 5622Mb
#2	INITED cov: 4 ft: 4 corp: 1/5b exec/s: 0 rss: 5622Mb
java.lang.IllegalArgumentException: ensureCountersAllocated() called with different numCounters for id -2041319264: existing=21, requested=19
	at com.code_intelligence.jazzer.runtime.CountersTracker.ensureCountersAllocated(CountersTracker.java:123)
	at com.code_intelligence.jazzer.api.Jazzer.maximize(Jazzer.java:289)
...
java.lang.IllegalArgumentException: ensureCountersAllocated() called with different numCounters for id -2041319264: existing=21, requested=14
...
java.lang.IllegalArgumentException: ensureCountersAllocated() called with different numCounters for id -2041319264: existing=21, requested=12
...
#10	DONE   cov: 4 ft: 4 corp: 1/5b lim: 4096 exec/s: 0 rss: 5622Mb
Done 10 runs in 0 second(s)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea: From the user perspective, would it make more sense to give them an object they can initialize beforehand and then call later in their code? Something like this:

static {
  CoverageAmplifier reactorAmplifier = new CoverageAmplifier(2000, 3000);
}

@FuzzTest
public void test(@InRange(min=10, max=20) int data) {
  coverageAmplifier.maximize(data);
  // or
  coverageAmplifier.minimize(data);
}

This might also improve performance quite a bit, because at the moment, we call ensureCountersAllocated every time in minimize, and the bound checks for the range are always performed, which only need to be done once.

Copy link
Contributor

@oetr oetr Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea: the current user range is a 1:1 mapping from values to the number of coverage counters.
A range [0, 2^20] takes up all the counters.
Why not map the user range to a fixed number of extra counters, say N=128 for now for ranges > 128, and 1:1 for ranges less than that? We can determine a good N by using the fuzzing benchmark.

This will make sure that a casual user doesn't flood the corpus with a million inputs by accident.
We could also add an option to specify how many counters should be used, for users who know what they are doing.

For example, this fuzz test gives me 60 exec/s:

 @FuzzTest(maxDuration = "0m")
  public void decompressRoundtrip(@InRange(min=0, max=20000) int high) {
    Jazzer.maximize(high, 0, 100000);
  }


#454	REDUCE cov: 20002 ft: 20002 corp: 7/29b lim: 4096 exec/s: 56 rss: 5761Mb L: 4/5 MS: 2 CopyPart-Custom-
#512	pulse  cov: 20002 ft: 20002 corp: 7/29b lim: 4096 exec/s: 56 rss: 5761Mb

Maybe the user can add the number of counters explicitly: Jazzer.maximize(high, 0, 100000, 1024);

if (temperature >= 4500) {
throw new RuntimeException("Meltdown! Temperature maximized.");
}
}
}
1 change: 1 addition & 0 deletions src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ java_library(
"FuzzerSecurityIssueMedium.java",
"HookType.java",
"Jazzer.java",
"JazzerApiException.java",
"MethodHook.java",
"MethodHooks.java",
"//src/main/java/jaz",
Expand Down
110 changes: 104 additions & 6 deletions src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,18 @@ public final class Jazzer {
private static final MethodHandle TRACE_MEMCMP;
private static final MethodHandle TRACE_PC_INDIR;

private static final MethodHandle COUNTERS_TRACKER_ALLOCATE;
private static final MethodHandle COUNTERS_TRACKER_SET_RANGE;

static {
Class<?> jazzerInternal = null;
MethodHandle onFuzzTargetReady = null;
MethodHandle traceStrcmp = null;
MethodHandle traceStrstr = null;
MethodHandle traceMemcmp = null;
MethodHandle tracePcIndir = null;
MethodHandle countersTrackerAllocate = null;
MethodHandle countersTrackerSetRange = null;
try {
jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal");
MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class);
Expand Down Expand Up @@ -70,6 +75,16 @@ public final class Jazzer {
tracePcIndir =
MethodHandles.publicLookup()
.findStatic(traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType);

Class<?> countersTracker =
Class.forName("com.code_intelligence.jazzer.runtime.CountersTracker");
MethodType allocateType = MethodType.methodType(void.class, int.class, int.class);
countersTrackerAllocate =
MethodHandles.publicLookup()
.findStatic(countersTracker, "ensureCountersAllocated", allocateType);
MethodType setRangeType = MethodType.methodType(void.class, int.class, int.class);
countersTrackerSetRange =
MethodHandles.publicLookup().findStatic(countersTracker, "setCounterRange", setRangeType);
} catch (ClassNotFoundException ignore) {
// Not running in the context of the agent. This is fine as long as no methods are called on
// this class.
Expand All @@ -86,14 +101,16 @@ public final class Jazzer {
TRACE_STRSTR = traceStrstr;
TRACE_MEMCMP = traceMemcmp;
TRACE_PC_INDIR = tracePcIndir;
COUNTERS_TRACKER_ALLOCATE = countersTrackerAllocate;
COUNTERS_TRACKER_SET_RANGE = countersTrackerSetRange;
}

private Jazzer() {}

/**
* A 32-bit random number that hooks can use to make pseudo-random choices between multiple
* possible mutations they could guide the fuzzer towards. Hooks <b>must not</b> base the decision
* whether or not to report a finding on this number as this will make findings non-reproducible.
* whether to report a finding on this number as this will make findings non-reproducible.
*
* <p>This is the same number that libFuzzer uses as a seed internally, which makes it possible to
* deterministically reproduce a previous fuzzing run by supplying the seed value printed by
Expand All @@ -119,8 +136,10 @@ public static void guideTowardsEquality(String current, String target, int id) {
}
try {
TRACE_STRCMP.invokeExact(current, target, 1, id);
} catch (JazzerApiException e) {
throw e;
} catch (Throwable e) {
e.printStackTrace();
throw new JazzerApiException("guideTowardsEquality: " + e.getMessage(), e);
}
}

Expand All @@ -142,8 +161,10 @@ public static void guideTowardsEquality(byte[] current, byte[] target, int id) {
}
try {
TRACE_MEMCMP.invokeExact(current, target, 1, id);
} catch (JazzerApiException e) {
throw e;
} catch (Throwable e) {
e.printStackTrace();
throw new JazzerApiException("guideTowardsEquality: " + e.getMessage(), e);
}
}

Expand All @@ -166,8 +187,10 @@ public static void guideTowardsContainment(String haystack, String needle, int i
}
try {
TRACE_STRSTR.invokeExact(haystack, needle, id);
} catch (JazzerApiException e) {
throw e;
} catch (Throwable e) {
e.printStackTrace();
throw new JazzerApiException("guideTowardsContainment: " + e.getMessage(), e);
}
}

Expand Down Expand Up @@ -212,8 +235,10 @@ public static void exploreState(byte state, int id) {
int upperBits = id >>> 5;
try {
TRACE_PC_INDIR.invokeExact(upperBits, lowerBits);
} catch (JazzerApiException e) {
throw e;
} catch (Throwable e) {
e.printStackTrace();
throw new JazzerApiException("exploreState: " + e.getMessage(), e);
}
}

Expand All @@ -230,6 +255,77 @@ public static void exploreState(byte state) {
// an automatically generated call-site id. Without instrumentation, this is a no-op.
}

/**
* Hill-climbing API to maximize a value. For each observed value v in [minValue, maxValue],
* provides feedback that all values in [minValue, v] are covered.
*
* <p>This enables corpus minimization to keep only the input resulting in the maximum value.
* Values below minValue provide no signal. Values above maxValue are clamped to maxValue.
*
* <p><b>Important:</b> This allocates (maxValue - minValue + 1) coverage counters per unique ID.
* For large value ranges, use a mapping function to reduce the range:
*
* <pre>{@code
* // Map [0, 1_000_000] to [0, 1000] steps
* long step = value < 0 ? 0 : Math.min(value / 1000, 1000);
* Jazzer.maximize(step, id, 0, 1000);
* }</pre>
*
* @param value The value to maximize (will be clamped to [minValue, maxValue])
* @param id A unique identifier for this call site (must be consistent across runs)
* @param minValue The minimum value in the range (inclusive)
* @param maxValue The maximum value in the range (inclusive)
*/
public static void maximize(long value, int id, long minValue, long maxValue) {
if (COUNTERS_TRACKER_ALLOCATE == null) {
return;
}

try {
if (maxValue < minValue) {
throw new IllegalArgumentException("maxValue must be >= minValue");
}
long range = maxValue - minValue;
if (range < 0 || range > (long) Integer.MAX_VALUE - 1) {
throw new IllegalArgumentException(
"Range too large: (maxValue - minValue + 1) must be <= Integer.MAX_VALUE");
}

int numCounters = (int) (range + 1);

// Allocate counters (idempotent, validates numCounters > 0 and consistency)
COUNTERS_TRACKER_ALLOCATE.invokeExact(id, numCounters);

// Set counters if value provides signal
if (value >= minValue) {
int toOffset = (int) (Math.min(value, maxValue) - minValue);
COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
}
} catch (JazzerApiException e) {
throw e;
} catch (Throwable e) {
throw new JazzerApiException("maximize: " + e.getMessage(), e);
}
}

/**
* Convenience overload of {@link #maximize(long, int, long, long)} that allows using
* automatically generated call-site identifiers. During instrumentation, calls to this method are
* replaced with calls to {@link #maximize(long, int, long, long)} using a unique id for each call
* site.
*
* <p>Without instrumentation, this is a no-op.
*
* @param value The value to maximize
* @param minValue The minimum value in the range (inclusive)
* @param maxValue The maximum value in the range (inclusive)
* @see #maximize(long, int, long, long)
*/
public static void maximize(long value, long minValue, long maxValue) {
// Instrumentation replaces calls to this method with calls to maximize(long, int, long, long)
// using an automatically generated call-site id. Without instrumentation, this is a no-op.
}

/**
* Make Jazzer report the provided {@link Throwable} as a finding.
*
Expand Down Expand Up @@ -261,8 +357,10 @@ public static void reportFindingFromHook(Throwable finding) {
public static void onFuzzTargetReady(Runnable callback) {
try {
ON_FUZZ_TARGET_READY.invokeExact(callback);
} catch (JazzerApiException e) {
throw e;
} catch (Throwable e) {
e.printStackTrace();
throw new JazzerApiException("onFuzzTargetReady: " + e.getMessage(), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2024 Code Intelligence GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.code_intelligence.jazzer.api;

/**
* Signals error from the Jazzer API (e.g. invalid arguments to {@link Jazzer#maximize}).
*
* <p>This exception is treated as a fatal error by the fuzzing engine rather than as a finding in
* the code under test. When thrown during fuzzing, it stops the current fuzz test with an error
* instead of reporting a bug in the fuzz target.
*/
public class JazzerApiException extends RuntimeException {
public JazzerApiException(String message) {
super(message);
}

public JazzerApiException(String message, Throwable cause) {
super(message, cause);
}

public JazzerApiException(Throwable cause) {
super(cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.code_intelligence.jazzer.driver;

import static com.code_intelligence.jazzer.driver.Constants.JAZZER_ERROR_EXIT_CODE;
import static com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE;
import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
import static java.lang.System.exit;
Expand Down Expand Up @@ -91,6 +92,8 @@ public final class FuzzTargetRunner {

private static final String OPENTEST4J_TEST_ABORTED_EXCEPTION =
"org.opentest4j.TestAbortedException";
private static final String JAZZER_API_EXCEPTION =
"com.code_intelligence.jazzer.api.JazzerApiException";

private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();

Expand Down Expand Up @@ -271,6 +274,16 @@ private static int runOne(long dataPtr, int dataLength) {
finding = JazzerInternal.lastFinding;
JazzerInternal.lastFinding = null;
}
// JazzerApiException signals API error, not a finding in the code under test.
if (finding != null && finding.getClass().getName().equals(JAZZER_API_EXCEPTION)) {
Log.error("Jazzer API error", finding);
temporarilyDisableLibfuzzerExitHook();
if (fatalFindingHandlerForJUnit != null) {
fatalFindingHandlerForJUnit.accept(finding);
return LIBFUZZER_RETURN_FROM_DRIVER;
}
exit(JAZZER_ERROR_EXIT_CODE);
}
// Allow skipping invalid inputs in fuzz tests by using e.g. JUnit's assumeTrue.
if (finding == null || finding.getClass().getName().equals(OPENTEST4J_TEST_ABORTED_EXCEPTION)) {
return LIBFUZZER_CONTINUE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import org.junit.platform.commons.support.AnnotationSupport;

class FuzzTestExecutor {
private static final String JAZZER_API_EXCEPTION =
"com.code_intelligence.jazzer.api.JazzerApiException";
private static final AtomicBoolean hasBeenPrepared = new AtomicBoolean();
private static final AtomicBoolean agentInstalled = new AtomicBoolean(false);

Expand Down Expand Up @@ -332,6 +334,9 @@ public Optional<Throwable> execute(
Throwable finding = atomicFinding.get();

if (finding != null) {
if (finding.getClass().getName().equals(JAZZER_API_EXCEPTION)) {
return Optional.of(finding);
}
return Optional.of(new FuzzTestFindingException(finding));
} else if (exitCode != 0) {
return Optional.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class FuzzTestExtensions
implements ExecutionCondition, InvocationInterceptor, TestExecutionExceptionHandler {
private static final String JAZZER_INTERNAL =
"com.code_intelligence.jazzer.runtime.JazzerInternal";
private static final String JAZZER_API_EXCEPTION =
"com.code_intelligence.jazzer.api.JazzerApiException";
private static final AtomicReference<Method> fuzzTestMethod = new AtomicReference<>();
private static Field lastFindingField;
private static Field hooksEnabledField;
Expand Down Expand Up @@ -112,6 +114,10 @@ private static void runWithHooks(Invocation<Void> invocation) throws Throwable {
} catch (Throwable t) {
thrown = t;
}
// JazzerApiException signals API error, so propagate as is and not as a finding.
if (thrown != null && thrown.getClass().getName().equals(JAZZER_API_EXCEPTION)) {
throw thrown;
}
Throwable stored = (Throwable) getLastFindingField().get(null);
if (stored != null) {
throw new FuzzTestFindingException(stored);
Expand Down
Loading