Skip to content

Commit 1c0deb4

Browse files
committed
feat: add maximize() hill-climbing API to Jazzer
Add Jazzer.maximize(value, minValue, maxValue) for guiding the fuzzer to maximize a value over time. The user's value range is linearly mapped onto a fixed number of coverage counters (default 1024), avoiding the risk of exhausting counter space with large ranges. The effective counter count is capped at the actual range size when the range is smaller than the requested number. Three overloads are provided: - maximize(value, minValue, maxValue) — convenience, auto-generated id - maximize(value, minValue, maxValue, numCounters) — custom counter count - maximize(value, minValue, maxValue, numCounters, id) — full control Also wraps all Jazzer API methods with JazzerApiException error handling and adds a ReactorFuzzTest example demonstrating the maximize API.
1 parent 2b9e788 commit 1c0deb4

File tree

6 files changed

+459
-6
lines changed

6 files changed

+459
-6
lines changed

examples/junit/src/test/java/com/example/BUILD.bazel

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,24 @@ java_fuzz_target_test(
372372
"@maven//:org_junit_jupiter_junit_jupiter_params",
373373
],
374374
)
375+
376+
# Test for the maximize() hill-climbing API.
377+
# This test uses Jazzer.maximize() to guide the fuzzer toward maximizing
378+
# a "temperature" value, demonstrating hill-climbing behavior.
379+
java_fuzz_target_test(
380+
name = "ReactorFuzzTest",
381+
srcs = ["ReactorFuzzTest.java"],
382+
allowed_findings = ["java.lang.RuntimeException"],
383+
env = {"JAZZER_FUZZ": "1"},
384+
target_class = "com.example.ReactorFuzzTest",
385+
verify_crash_reproducer = False,
386+
runtime_deps = [
387+
":junit_runtime",
388+
],
389+
deps = [
390+
"//src/main/java/com/code_intelligence/jazzer/api:hooks",
391+
"//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test",
392+
"//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
393+
"@maven//:org_junit_jupiter_junit_jupiter_api",
394+
],
395+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example;
18+
19+
import com.code_intelligence.jazzer.api.Jazzer;
20+
import com.code_intelligence.jazzer.junit.FuzzTest;
21+
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
22+
23+
public class ReactorFuzzTest {
24+
25+
@FuzzTest
26+
public void fuzz(@NotNull String input) {
27+
for (char c : input.toCharArray()) {
28+
if (c < 32 || c > 126) return;
29+
}
30+
controlReactor(input);
31+
}
32+
33+
private void controlReactor(String commands) {
34+
long temperature = 0; // Starts cold
35+
36+
for (char cmd : commands.toCharArray()) {
37+
// Complex, chaotic feedback loop.
38+
// It is hard to predict which character increases temperature
39+
// because it depends on the CURRENT temperature.
40+
if ((temperature ^ cmd) % 3 == 0) {
41+
temperature += (cmd % 10); // Heat up slightly
42+
} else if ((temperature ^ cmd) % 3 == 1) {
43+
temperature -= (cmd % 8); // Cool down slightly
44+
} else {
45+
temperature += 1; // Tiny increase
46+
}
47+
48+
// Prevent dropping below absolute zero for simulation sanity
49+
if (temperature < 0) temperature = 0;
50+
}
51+
// THE GOAL: MAXIMIZATION
52+
// We need to drive 'temperature' to an extreme value.
53+
// Standard coverage is 100% constant here (it just loops).
54+
Jazzer.maximize(temperature, 0, 4500);
55+
if (temperature >= 4500) {
56+
throw new RuntimeException("Meltdown! Temperature maximized.");
57+
}
58+
}
59+
}

src/main/java/com/code_intelligence/jazzer/api/Jazzer.java

Lines changed: 158 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.lang.invoke.MethodType;
2222
import java.lang.reflect.InvocationTargetException;
2323
import java.security.SecureRandom;
24+
import java.util.concurrent.ConcurrentHashMap;
2425

2526
/** Static helper methods that hooks can use to provide feedback to the fuzzer. */
2627
public final class Jazzer {
@@ -33,13 +34,28 @@ public final class Jazzer {
3334
private static final MethodHandle TRACE_MEMCMP;
3435
private static final MethodHandle TRACE_PC_INDIR;
3536

37+
private static final MethodHandle COUNTERS_TRACKER_ALLOCATE;
38+
private static final MethodHandle COUNTERS_TRACKER_SET_RANGE;
39+
40+
/**
41+
* Default number of counters allocated for each call site of a method that requires registering a
42+
* range of artificial coverage counters, e.g., Jazzer maximize API. The user's value range is
43+
* linearly mapped onto this many counters.
44+
*/
45+
public static final int DEFAULT_NUM_COUNTERS = 1024;
46+
47+
/** Tracks the registered minValue and maxValue per maximize call-site id. */
48+
private static final ConcurrentHashMap<Integer, long[]> idToRange = new ConcurrentHashMap<>();
49+
3650
static {
3751
Class<?> jazzerInternal = null;
3852
MethodHandle onFuzzTargetReady = null;
3953
MethodHandle traceStrcmp = null;
4054
MethodHandle traceStrstr = null;
4155
MethodHandle traceMemcmp = null;
4256
MethodHandle tracePcIndir = null;
57+
MethodHandle countersTrackerAllocate = null;
58+
MethodHandle countersTrackerSetRange = null;
4359
try {
4460
jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal");
4561
MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class);
@@ -70,6 +86,16 @@ public final class Jazzer {
7086
tracePcIndir =
7187
MethodHandles.publicLookup()
7288
.findStatic(traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType);
89+
90+
Class<?> countersTracker =
91+
Class.forName("com.code_intelligence.jazzer.runtime.ExtraCountersTracker");
92+
MethodType allocateType = MethodType.methodType(void.class, int.class, int.class);
93+
countersTrackerAllocate =
94+
MethodHandles.publicLookup()
95+
.findStatic(countersTracker, "ensureCountersAllocated", allocateType);
96+
MethodType setRangeType = MethodType.methodType(void.class, int.class, int.class);
97+
countersTrackerSetRange =
98+
MethodHandles.publicLookup().findStatic(countersTracker, "setCounterRange", setRangeType);
7399
} catch (ClassNotFoundException ignore) {
74100
// Not running in the context of the agent. This is fine as long as no methods are called on
75101
// this class.
@@ -86,14 +112,16 @@ public final class Jazzer {
86112
TRACE_STRSTR = traceStrstr;
87113
TRACE_MEMCMP = traceMemcmp;
88114
TRACE_PC_INDIR = tracePcIndir;
115+
COUNTERS_TRACKER_ALLOCATE = countersTrackerAllocate;
116+
COUNTERS_TRACKER_SET_RANGE = countersTrackerSetRange;
89117
}
90118

91119
private Jazzer() {}
92120

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

@@ -142,8 +172,10 @@ public static void guideTowardsEquality(byte[] current, byte[] target, int id) {
142172
}
143173
try {
144174
TRACE_MEMCMP.invokeExact(current, target, 1, id);
175+
} catch (JazzerApiException e) {
176+
throw e;
145177
} catch (Throwable e) {
146-
e.printStackTrace();
178+
throw new JazzerApiException("guideTowardsEquality: " + e.getMessage(), e);
147179
}
148180
}
149181

@@ -166,8 +198,10 @@ public static void guideTowardsContainment(String haystack, String needle, int i
166198
}
167199
try {
168200
TRACE_STRSTR.invokeExact(haystack, needle, id);
201+
} catch (JazzerApiException e) {
202+
throw e;
169203
} catch (Throwable e) {
170-
e.printStackTrace();
204+
throw new JazzerApiException("guideTowardsContainment: " + e.getMessage(), e);
171205
}
172206
}
173207

@@ -212,8 +246,10 @@ public static void exploreState(byte state, int id) {
212246
int upperBits = id >>> 5;
213247
try {
214248
TRACE_PC_INDIR.invokeExact(upperBits, lowerBits);
249+
} catch (JazzerApiException e) {
250+
throw e;
215251
} catch (Throwable e) {
216-
e.printStackTrace();
252+
throw new JazzerApiException("exploreState: " + e.getMessage(), e);
217253
}
218254
}
219255

@@ -230,6 +266,120 @@ public static void exploreState(byte state) {
230266
// an automatically generated call-site id. Without instrumentation, this is a no-op.
231267
}
232268

269+
/**
270+
* Core implementation of the hill-climbing maximize API. It maps {@code value} from the range
271+
* [{@code minValue}, {@code maxValue}] onto {@code numCounters} coverage counters via linear
272+
* interpolation, then sets all counters from 0 to the mapped offset.
273+
*
274+
* <p>Values below {@code minValue} produce no signal. Values above {@code maxValue} are clamped.
275+
*
276+
* <p>Must be invoked with the same {@code minValue}, {@code maxValue}, and {@code numCounters}
277+
* for a given {@code id} across all calls. Passing different values is illegal.
278+
*
279+
* @param value the value to maximize
280+
* @param minValue the minimum expected value (inclusive)
281+
* @param maxValue the maximum expected value (inclusive); must be &gt;= {@code minValue}
282+
* @param numCounters the number of counters to allocate; must be &gt; 0
283+
* @param id a unique identifier for this call site (must be consistent across runs)
284+
* @throws JazzerApiException if {@code maxValue < minValue} or {@code numCounters <= 0}
285+
*/
286+
public static void maximize(long value, long minValue, long maxValue, int numCounters, int id) {
287+
if (COUNTERS_TRACKER_ALLOCATE == null) {
288+
return;
289+
}
290+
291+
try {
292+
ensureRangeConsistent(id, minValue, maxValue);
293+
int effectiveCounters = effectiveCounters(minValue, maxValue, numCounters);
294+
COUNTERS_TRACKER_ALLOCATE.invokeExact(id, effectiveCounters);
295+
296+
if (value >= minValue) {
297+
int toOffset;
298+
if (minValue == maxValue) {
299+
toOffset = 0;
300+
} else {
301+
double range = (double) maxValue - (double) minValue;
302+
double offset = (double) Math.min(value, maxValue) - (double) minValue;
303+
toOffset = (int) (offset / range * (effectiveCounters - 1));
304+
}
305+
COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
306+
}
307+
} catch (JazzerApiException e) {
308+
throw e;
309+
} catch (Throwable e) {
310+
throw new JazzerApiException("maximize: " + e.getMessage(), e);
311+
}
312+
}
313+
314+
private static void ensureRangeConsistent(int id, long minValue, long maxValue) {
315+
long[] existing = idToRange.putIfAbsent(id, new long[] {minValue, maxValue});
316+
if (existing != null && (existing[0] != minValue || existing[1] != maxValue)) {
317+
throw new IllegalArgumentException(
318+
String.format(
319+
"Range for id %d must remain constant across calls. "
320+
+ "Expected [%d, %d], but got [%d, %d].",
321+
id, existing[0], existing[1], minValue, maxValue));
322+
}
323+
}
324+
325+
private static int effectiveCounters(long minValue, long maxValue, int maxNumCounters) {
326+
if (maxValue < minValue) {
327+
throw new IllegalArgumentException(
328+
"maxValue (" + maxValue + ") must not be less than minValue (" + minValue + ")");
329+
}
330+
if (maxNumCounters <= 0) {
331+
throw new IllegalArgumentException(
332+
"maxNumCounters (" + maxNumCounters + ") must be positive");
333+
}
334+
335+
// Cap maxNumCounters at the actual range size to avoid wasting counters when the
336+
// range is smaller than the requested number (e.g. range [0, 10] only needs 11).
337+
double rangeSize = (double) maxValue - (double) minValue + 1;
338+
return (rangeSize < maxNumCounters) ? (int) rangeSize : maxNumCounters;
339+
}
340+
341+
/**
342+
* Convenience overload of {@link #maximize(long, long, long, int, int)} that uses {@link
343+
* #DEFAULT_NUM_COUNTERS} counters and an automatically generated call-site id.
344+
*
345+
* <p>During instrumentation, calls to this method are replaced by a hook that supplies a unique
346+
* id for each call site. Without instrumentation, this is a no-op.
347+
*
348+
* <pre>{@code
349+
* // Maximize temperature in [0, 4500]
350+
* Jazzer.maximize(temperature, 0, 4500);
351+
* }</pre>
352+
*
353+
* @param value the value to maximize
354+
* @param minValue the minimum expected value (inclusive)
355+
* @param maxValue the maximum expected value (inclusive)
356+
* @see #maximize(long, long, long, int, int)
357+
*/
358+
public static void maximize(long value, long minValue, long maxValue) {
359+
// Instrumentation replaces calls to this method with the core overload using
360+
// DEFAULT_NUM_COUNTERS and an automatically generated call-site id.
361+
// Without instrumentation, this is a no-op.
362+
}
363+
364+
/**
365+
* Convenience overload of {@link #maximize(long, long, long, int, int)} that uses a custom number
366+
* of counters and an automatically generated call-site id.
367+
*
368+
* <p>During instrumentation, calls to this method are replaced by a hook that supplies a unique
369+
* id for each call site. Without instrumentation, this is a no-op.
370+
*
371+
* @param value the value to maximize
372+
* @param minValue the minimum expected value (inclusive)
373+
* @param maxValue the maximum expected value (inclusive)
374+
* @param numCounters the number of counters to allocate; must be &gt; 0
375+
* @see #maximize(long, long, long, int, int)
376+
*/
377+
public static void maximize(long value, long minValue, long maxValue, int numCounters) {
378+
// Instrumentation replaces calls to this method with the core overload using
379+
// the given numCounters and an automatically generated call-site id.
380+
// Without instrumentation, this is a no-op.
381+
}
382+
233383
/**
234384
* Make Jazzer report the provided {@link Throwable} as a finding.
235385
*
@@ -261,8 +411,10 @@ public static void reportFindingFromHook(Throwable finding) {
261411
public static void onFuzzTargetReady(Runnable callback) {
262412
try {
263413
ON_FUZZ_TARGET_READY.invokeExact(callback);
414+
} catch (JazzerApiException e) {
415+
throw e;
264416
} catch (Throwable e) {
265-
e.printStackTrace();
417+
throw new JazzerApiException("onFuzzTargetReady: " + e.getMessage(), e);
266418
}
267419
}
268420

src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,39 @@ public static void exploreStateWithId(
4343
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
4444
Jazzer.exploreState((byte) arguments[0], hookId);
4545
}
46+
47+
/**
48+
* Replaces calls to {@link Jazzer#maximize(long, long, long)} with calls to {@link
49+
* Jazzer#maximize(long, long, long, int, int)} using {@link Jazzer#DEFAULT_NUM_COUNTERS} and the
50+
* hook id.
51+
*/
52+
@MethodHook(
53+
type = HookType.REPLACE,
54+
targetClassName = "com.code_intelligence.jazzer.api.Jazzer",
55+
targetMethod = "maximize",
56+
targetMethodDescriptor = "(JJJ)V")
57+
public static void maximizeWithDefaultCountersAndId(
58+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
59+
Jazzer.maximize(
60+
(long) arguments[0],
61+
(long) arguments[1],
62+
(long) arguments[2],
63+
Jazzer.DEFAULT_NUM_COUNTERS,
64+
hookId);
65+
}
66+
67+
/**
68+
* Replaces calls to {@link Jazzer#maximize(long, long, long, int)} with calls to {@link
69+
* Jazzer#maximize(long, long, long, int, int)} using the hook id.
70+
*/
71+
@MethodHook(
72+
type = HookType.REPLACE,
73+
targetClassName = "com.code_intelligence.jazzer.api.Jazzer",
74+
targetMethod = "maximize",
75+
targetMethodDescriptor = "(JJJI)V")
76+
public static void maximizeWithCustomCountersAndId(
77+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
78+
Jazzer.maximize(
79+
(long) arguments[0], (long) arguments[1], (long) arguments[2], (int) arguments[3], hookId);
80+
}
4681
}

0 commit comments

Comments
 (0)