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);
Copy link
Contributor

Choose a reason for hiding this comment

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

This version forces the users to write mappers every time they use maximize. It's easy to make mistake when writing them. I think we should automatically map the given user range to our range.

I liked the old API version more, it just lacked the mapping to fixed range of IDs, and runtime checks that the parameters stay constant, but it was easier to use.
Consider Jazzer.maximize(value, a, b) --- when the value is between a to b, there will be different coverage (between 0 to 1023), below a nothing, and after b just max coverage.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider making it final, to signal that we do not plan to subclass it and add error hierarchies.

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