Skip to content
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",
],
)
60 changes: 60 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,60 @@
/*
* 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).
long mapped = temperature * 1023 / 4500;
Jazzer.maximize(mapped);
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
99 changes: 93 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,24 @@ 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;

/**
* Fixed number of counters per hill-climbing call site. Users must map their domain values into
* [0, 1023] before calling {@link #maximize(long)} or {@link #maximize(long, int)}.
*/
private static final int HILL_CLIMBING_RANGE = 1024;

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 +81,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 +107,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 +142,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 +167,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 +193,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 +241,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 +261,60 @@ 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 [0, 1023], provides
* feedback that all values in [0, v] are covered.
*
* <p>This enables corpus minimization to keep only the input resulting in the maximum value.
* Values below 0 provide no signal. Values above 1023 are clamped to 1023.
*
* <p>Each call site allocates exactly 1024 coverage counters. Map your domain values into [0,
* 1023] before calling this method:
*
* <pre>{@code
* // Map temperature in [500, 4500] to [0, 1023]
* long mapped = (temperature - 500) * 1023 / (4500 - 500);
* Jazzer.maximize(mapped);
* }</pre>
*
* @param value The value to maximize (expected in [0, 1023]; negative values produce no signal,
* values above 1023 are clamped)
* @param id A unique identifier for this call site (must be consistent across runs)
*/
public static void maximize(long value, int id) {
if (COUNTERS_TRACKER_ALLOCATE == null) {
return;
}

try {
COUNTERS_TRACKER_ALLOCATE.invokeExact(id, HILL_CLIMBING_RANGE);

if (value >= 0) {
int toOffset = (int) Math.min(value, HILL_CLIMBING_RANGE - 1);
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)} that allows using automatically generated
* call-site identifiers. During instrumentation, calls to this method are replaced with calls to
* {@link #maximize(long, int)} using a unique id for each call site.
*
* <p>Without instrumentation, this is a no-op.
*
* @param value The value to maximize (expected in [0, 1023])
* @see #maximize(long, int)
*/
public static void maximize(long value) {
// Instrumentation replaces calls to this method with calls to maximize(long, int)
// 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 +346,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