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);
if (temperature >= 4500) {
throw new RuntimeException("Meltdown! Temperature maximized.");
}
}
}
88 changes: 87 additions & 1 deletion 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 Down Expand Up @@ -230,6 +247,75 @@ 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 (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");
}
Comment on lines +272 to +279
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The validation checks for invalid ranges occur before checking if the runtime is available (COUNTERS_TRACKER_ALLOCATE == null). This causes exceptions to be thrown even when running without the Jazzer runtime, which is inconsistent with the behavior of similar methods like exploreState() and guideTowardsContainment() that return early when the runtime is unavailable. The validation should be moved after the null check, or wrapped in a try-catch that returns silently, to ensure the method is a no-op when the runtime is not available.

Copilot uses AI. Check for mistakes.

if (COUNTERS_TRACKER_ALLOCATE == null) {
return;
}

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

try {
// 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);
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

Integer overflow in toOffset calculation: Similar to the numCounters calculation above, when (Math.min(value, maxValue) - minValue) exceeds Integer.MAX_VALUE, the cast to int will silently overflow. This could cause incorrect counter ranges to be set. Add validation before the cast.

Suggested change
int toOffset = (int) (Math.min(value, maxValue) - minValue);
long toOffsetLong = Math.min(value, maxValue) - minValue;
if (toOffsetLong > Integer.MAX_VALUE) {
// Avoid integer overflow when converting to int; cannot represent this offset safely.
return;
}
int toOffset = (int) toOffsetLong;

Copilot uses AI. Check for mistakes.
COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
}
} catch (Throwable e) {
e.printStackTrace();
}
}

/**
* 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
17 changes: 17 additions & 0 deletions src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ java_library(

# The following targets must only be referenced directly by tests or native implementations.

java_jni_library(
name = "counters_tracker",
srcs = ["CountersTracker.java"],
native_libs = select({
"@platforms//os:android": ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"],
"//conditions:default": [],
}),
visibility = [
"//src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
"//src/test:__subpackages__",
],
deps = [
"//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
],
)

java_jni_library(
name = "coverage_map",
srcs = ["CoverageMap.java"],
Expand Down Expand Up @@ -170,6 +186,7 @@ java_library(
],
deps = [
":constants",
":counters_tracker",
":coverage_map",
":trace_data_flow_native_callbacks",
"//src/main/java/com/code_intelligence/jazzer/api:hooks",
Expand Down
Loading
Loading