Skip to content
Merged
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, 0, 4500);
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
164 changes: 158 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 @@ -21,6 +21,7 @@
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.security.SecureRandom;
import java.util.concurrent.ConcurrentHashMap;

/** Static helper methods that hooks can use to provide feedback to the fuzzer. */
public final class Jazzer {
Expand All @@ -33,13 +34,28 @@ 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;

/**
* Default number of counters allocated for each call site of a method that requires registering a
* range of artificial coverage counters, e.g., Jazzer maximize API. The user's value range is
* linearly mapped onto this many counters.
*/
public static final int DEFAULT_NUM_COUNTERS = 1024;

/** Tracks the registered minValue and maxValue per maximize call-site id. */
private static final ConcurrentHashMap<Integer, long[]> idToRange = new ConcurrentHashMap<>();

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 +86,16 @@ public final class Jazzer {
tracePcIndir =
MethodHandles.publicLookup()
.findStatic(traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType);

Class<?> countersTracker =
Class.forName("com.code_intelligence.jazzer.runtime.ExtraCountersTracker");
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 +112,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 +147,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 +172,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 +198,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 +246,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 +266,120 @@ public static void exploreState(byte state) {
// an automatically generated call-site id. Without instrumentation, this is a no-op.
}

/**
* Core implementation of the hill-climbing maximize API. It maps {@code value} from the range
* [{@code minValue}, {@code maxValue}] onto {@code numCounters} coverage counters via linear
* interpolation, then sets all counters from 0 to the mapped offset.
*
* <p>Values below {@code minValue} produce no signal. Values above {@code maxValue} are clamped.
*
* <p>Must be invoked with the same {@code minValue}, {@code maxValue}, and {@code numCounters}
* for a given {@code id} across all calls. Passing different values is illegal.
*
* @param value the value to maximize
* @param minValue the minimum expected value (inclusive)
* @param maxValue the maximum expected value (inclusive); must be &gt;= {@code minValue}
* @param numCounters the number of counters to allocate; must be &gt; 0
* @param id a unique identifier for this call site (must be consistent across runs)
* @throws JazzerApiException if {@code maxValue < minValue} or {@code numCounters <= 0}
*/
public static void maximize(long value, long minValue, long maxValue, int numCounters, int id) {
if (COUNTERS_TRACKER_ALLOCATE == null) {
return;
}

try {
ensureRangeConsistent(id, minValue, maxValue);
int effectiveCounters = effectiveCounters(minValue, maxValue, numCounters);
COUNTERS_TRACKER_ALLOCATE.invokeExact(id, effectiveCounters);

if (value >= minValue) {
int toOffset;
if (minValue == maxValue) {
toOffset = 0;
} else {
double range = (double) maxValue - (double) minValue;
double offset = (double) Math.min(value, maxValue) - (double) minValue;
toOffset = (int) (offset / range * (effectiveCounters - 1));
}
COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
}
} catch (JazzerApiException e) {
throw e;
} catch (Throwable e) {
throw new JazzerApiException("maximize: " + e.getMessage(), e);
}
}

private static void ensureRangeConsistent(int id, long minValue, long maxValue) {
long[] existing = idToRange.putIfAbsent(id, new long[] {minValue, maxValue});
if (existing != null && (existing[0] != minValue || existing[1] != maxValue)) {
throw new IllegalArgumentException(
String.format(
"Range for id %d must remain constant across calls. "
+ "Expected [%d, %d], but got [%d, %d].",
id, existing[0], existing[1], minValue, maxValue));
}
}

private static int effectiveCounters(long minValue, long maxValue, int maxNumCounters) {
if (maxValue < minValue) {
throw new IllegalArgumentException(
"maxValue (" + maxValue + ") must not be less than minValue (" + minValue + ")");
}
if (maxNumCounters <= 0) {
throw new IllegalArgumentException(
"maxNumCounters (" + maxNumCounters + ") must be positive");
}

// Cap maxNumCounters at the actual range size to avoid wasting counters when the
// range is smaller than the requested number (e.g. range [0, 10] only needs 11).
double rangeSize = (double) maxValue - (double) minValue + 1;
return (rangeSize < maxNumCounters) ? (int) rangeSize : maxNumCounters;
}

/**
* Convenience overload of {@link #maximize(long, long, long, int, int)} that uses {@link
* #DEFAULT_NUM_COUNTERS} counters and an automatically generated call-site id.
*
* <p>During instrumentation, calls to this method are replaced by a hook that supplies a unique
* id for each call site. Without instrumentation, this is a no-op.
*
* <pre>{@code
* // Maximize temperature in [0, 4500]
* Jazzer.maximize(temperature, 0, 4500);
* }</pre>
*
* @param value the value to maximize
* @param minValue the minimum expected value (inclusive)
* @param maxValue the maximum expected value (inclusive)
* @see #maximize(long, long, long, int, int)
*/
public static void maximize(long value, long minValue, long maxValue) {
// Instrumentation replaces calls to this method with the core overload using
// DEFAULT_NUM_COUNTERS and an automatically generated call-site id.
// Without instrumentation, this is a no-op.
}

/**
* Convenience overload of {@link #maximize(long, long, long, int, int)} that uses a custom number
* of counters and an automatically generated call-site id.
*
* <p>During instrumentation, calls to this method are replaced by a hook that supplies a unique
* id for each call site. Without instrumentation, this is a no-op.
*
* @param value the value to maximize
* @param minValue the minimum expected value (inclusive)
* @param maxValue the maximum expected value (inclusive)
* @param numCounters the number of counters to allocate; must be &gt; 0
* @see #maximize(long, long, long, int, int)
*/
public static void maximize(long value, long minValue, long maxValue, int numCounters) {
// Instrumentation replaces calls to this method with the core overload using
// the given numCounters and 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 +411,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 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.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 final 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);
}
}
Loading