2121import java .lang .invoke .MethodType ;
2222import java .lang .reflect .InvocationTargetException ;
2323import java .security .SecureRandom ;
24+ import java .util .concurrent .ConcurrentHashMap ;
2425
2526/** Static helper methods that hooks can use to provide feedback to the fuzzer. */
2627public 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 >= {@code minValue}
282+ * @param numCounters the number of counters to allocate; must be > 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 > 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
0 commit comments