Skip to content

Commit c61a728

Browse files
kyakdanoetr
authored andcommitted
feat: add minimize API
1 parent 305c6e2 commit c61a728

4 files changed

Lines changed: 303 additions & 0 deletions

File tree

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,95 @@ public static void maximize(long value, long minValue, long maxValue, int numCou
380380
// Without instrumentation, this is a no-op.
381381
}
382382

383+
/**
384+
* Core implementation of the hill-climbing minimize API. It maps {@code value} from the range
385+
* [{@code minValue}, {@code maxValue}] onto {@code numCounters} coverage counters via inverse
386+
* linear interpolation, then sets all counters from 0 to the mapped offset.
387+
*
388+
* <p>Lower values produce more signal (more counters set), which causes the fuzzer to prefer
389+
* inputs that result in lower values. Values above {@code maxValue} produce no signal. Values
390+
* below {@code minValue} are clamped.
391+
*
392+
* <p>Must be invoked with the same {@code minValue}, {@code maxValue}, and {@code numCounters}
393+
* for a given {@code id} across all calls. Passing different values is illegal.
394+
*
395+
* @param value the value to minimize
396+
* @param minValue the minimum expected value (inclusive)
397+
* @param maxValue the maximum expected value (inclusive); must be &gt;= {@code minValue}
398+
* @param numCounters the number of counters to allocate; must be &gt; 0
399+
* @param id a unique identifier for this call site (must be consistent across runs)
400+
* @throws JazzerApiException if {@code maxValue < minValue} or {@code numCounters <= 0}
401+
*/
402+
public static void minimize(long value, long minValue, long maxValue, int numCounters, int id) {
403+
if (COUNTERS_TRACKER_ALLOCATE == null) {
404+
return;
405+
}
406+
407+
try {
408+
ensureRangeConsistent(id, minValue, maxValue);
409+
int effectiveCounters = effectiveCounters(minValue, maxValue, numCounters);
410+
COUNTERS_TRACKER_ALLOCATE.invokeExact(id, effectiveCounters);
411+
412+
if (value <= maxValue) {
413+
int toOffset;
414+
if (minValue == maxValue) {
415+
toOffset = 0;
416+
} else {
417+
double range = (double) maxValue - (double) minValue;
418+
double offset = (double) maxValue - (double) Math.max(value, minValue);
419+
toOffset = (int) (offset / range * (effectiveCounters - 1));
420+
}
421+
COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
422+
}
423+
} catch (JazzerApiException e) {
424+
throw e;
425+
} catch (Throwable e) {
426+
throw new JazzerApiException("minimize: " + e.getMessage(), e);
427+
}
428+
}
429+
430+
/**
431+
* Convenience overload of {@link #minimize(long, long, long, int, int)} that uses {@link
432+
* #DEFAULT_NUM_COUNTERS} counters and an automatically generated call-site id.
433+
*
434+
* <p>During instrumentation, calls to this method are replaced by a hook that supplies a unique
435+
* id for each call site. Without instrumentation, this is a no-op.
436+
*
437+
* <pre>{@code
438+
* // Minimize temperature in [0, 4000]
439+
* Jazzer.minimize(temperature, 0, 4000);
440+
* }</pre>
441+
*
442+
* @param value the value to minimize
443+
* @param minValue the minimum expected value (inclusive)
444+
* @param maxValue the maximum expected value (inclusive)
445+
* @see #minimize(long, long, long, int, int)
446+
*/
447+
public static void minimize(long value, long minValue, long maxValue) {
448+
// Instrumentation replaces calls to this method with the core overload using
449+
// DEFAULT_NUM_COUNTERS and an automatically generated call-site id.
450+
// Without instrumentation, this is a no-op.
451+
}
452+
453+
/**
454+
* Convenience overload of {@link #minimize(long, long, long, int, int)} that uses a custom number
455+
* of counters and an automatically generated call-site id.
456+
*
457+
* <p>During instrumentation, calls to this method are replaced by a hook that supplies a unique
458+
* id for each call site. Without instrumentation, this is a no-op.
459+
*
460+
* @param value the value to minimize
461+
* @param minValue the minimum expected value (inclusive)
462+
* @param maxValue the maximum expected value (inclusive)
463+
* @param numCounters the number of counters to allocate; must be &gt; 0
464+
* @see #minimize(long, long, long, int, int)
465+
*/
466+
public static void minimize(long value, long minValue, long maxValue, int numCounters) {
467+
// Instrumentation replaces calls to this method with the core overload using
468+
// the given numCounters and an automatically generated call-site id.
469+
// Without instrumentation, this is a no-op.
470+
}
471+
383472
/**
384473
* Make Jazzer report the provided {@link Throwable} as a finding.
385474
*

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,39 @@ public static void maximizeWithCustomCountersAndId(
7878
Jazzer.maximize(
7979
(long) arguments[0], (long) arguments[1], (long) arguments[2], (int) arguments[3], hookId);
8080
}
81+
82+
/**
83+
* Replaces calls to {@link Jazzer#minimize(long, long, long)} with calls to {@link
84+
* Jazzer#minimize(long, long, long, int, int)} using {@link Jazzer#DEFAULT_NUM_COUNTERS} and the
85+
* hook id.
86+
*/
87+
@MethodHook(
88+
type = HookType.REPLACE,
89+
targetClassName = "com.code_intelligence.jazzer.api.Jazzer",
90+
targetMethod = "minimize",
91+
targetMethodDescriptor = "(JJJ)V")
92+
public static void minimizeWithDefaultCountersAndId(
93+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
94+
Jazzer.minimize(
95+
(long) arguments[0],
96+
(long) arguments[1],
97+
(long) arguments[2],
98+
Jazzer.DEFAULT_NUM_COUNTERS,
99+
hookId);
100+
}
101+
102+
/**
103+
* Replaces calls to {@link Jazzer#minimize(long, long, long, int)} with calls to {@link
104+
* Jazzer#minimize(long, long, long, int, int)} using the hook id.
105+
*/
106+
@MethodHook(
107+
type = HookType.REPLACE,
108+
targetClassName = "com.code_intelligence.jazzer.api.Jazzer",
109+
targetMethod = "minimize",
110+
targetMethodDescriptor = "(JJJI)V")
111+
public static void minimizeWithCustomCountersAndId(
112+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
113+
Jazzer.minimize(
114+
(long) arguments[0], (long) arguments[1], (long) arguments[2], (int) arguments[3], hookId);
115+
}
81116
}

src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,22 @@ java_test(
4141
"@maven//:junit_junit",
4242
],
4343
)
44+
45+
java_test(
46+
name = "MinimizeTest",
47+
size = "small",
48+
srcs = [
49+
"MinimizeTest.java",
50+
],
51+
target_compatible_with = SKIP_ON_WINDOWS,
52+
test_class = "com.code_intelligence.jazzer.api.MinimizeTest",
53+
runtime_deps = [
54+
"//src/main/java/com/code_intelligence/jazzer/runtime",
55+
],
56+
deps = [
57+
"//src/main/java/com/code_intelligence/jazzer/api",
58+
"//src/main/java/com/code_intelligence/jazzer/api:hooks",
59+
"//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver",
60+
"@maven//:junit_junit",
61+
],
62+
)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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.code_intelligence.jazzer.api;
18+
19+
import static org.junit.Assert.assertTrue;
20+
import static org.junit.Assert.fail;
21+
22+
import org.junit.Test;
23+
24+
public class MinimizeTest {
25+
26+
@Test
27+
public void testBasicRangeMapping() {
28+
// value=50 in [0, 100] with 1024 counters
29+
// offset = (100 - 50) / 100 * 1023 = 511
30+
Jazzer.minimize(50, 0, 100, 1024, 600000);
31+
}
32+
33+
@Test
34+
public void testValueAtMinimum() {
35+
// value == minValue → offset = effectiveCounters - 1 (maximum signal)
36+
Jazzer.minimize(0, 0, 100, 1024, 600001);
37+
}
38+
39+
@Test
40+
public void testValueAtMaximum() {
41+
// value == maxValue → offset = 0 (minimum signal)
42+
Jazzer.minimize(100, 0, 100, 1024, 600002);
43+
}
44+
45+
@Test
46+
public void testValueAboveMaximum() {
47+
// value > maxValue → no signal (should not throw)
48+
Jazzer.minimize(200, 0, 100, 1024, 600003);
49+
}
50+
51+
@Test
52+
public void testValueBelowMinimum() {
53+
// value < minValue → clamped to minValue (offset = effectiveCounters - 1)
54+
Jazzer.minimize(-10, 0, 100, 1024, 600004);
55+
}
56+
57+
@Test
58+
public void testNegativeRange() {
59+
// Range with negative values: [-100, -50]
60+
Jazzer.minimize(-75, -100, -50, 1024, 600005);
61+
}
62+
63+
@Test
64+
public void testSingleValueRange() {
65+
// minValue == maxValue → offset is always 0
66+
Jazzer.minimize(42, 42, 42, 1024, 600006);
67+
}
68+
69+
@Test
70+
public void testLargeRange() {
71+
// Long.MIN_VALUE to Long.MAX_VALUE — should not overflow
72+
Jazzer.minimize(0, Long.MIN_VALUE, Long.MAX_VALUE, 1024, 600007);
73+
}
74+
75+
@Test
76+
public void testCustomNumCounters() {
77+
// Expert overload with small counter count
78+
Jazzer.minimize(50, 0, 100, 10, 600008);
79+
}
80+
81+
@Test
82+
public void testZeroNumCountersThrows() {
83+
try {
84+
Jazzer.minimize(50, 0, 100, 0, 600009);
85+
fail("Expected JazzerApiException for zero numCounters");
86+
} catch (JazzerApiException e) {
87+
assertTrue(e.getMessage().contains("must be positive"));
88+
}
89+
}
90+
91+
@Test
92+
public void testNegativeNumCountersThrows() {
93+
try {
94+
Jazzer.minimize(50, 0, 100, -5, 600010);
95+
fail("Expected JazzerApiException for negative numCounters");
96+
} catch (JazzerApiException e) {
97+
assertTrue(e.getMessage().contains("must be positive"));
98+
}
99+
}
100+
101+
@Test
102+
public void testMaxValueLessThanMinValueThrows() {
103+
try {
104+
Jazzer.minimize(50, 100, 0, 1024, 600011);
105+
fail("Expected JazzerApiException for maxValue < minValue");
106+
} catch (JazzerApiException e) {
107+
assertTrue(e.getMessage().contains("must not be less than"));
108+
}
109+
}
110+
111+
@Test
112+
public void testMultipleCallsSameId() {
113+
// Multiple calls with the same id should succeed (idempotent allocation)
114+
Jazzer.minimize(10, 0, 100, 1024, 600012);
115+
Jazzer.minimize(50, 0, 100, 1024, 600012);
116+
Jazzer.minimize(90, 0, 100, 1024, 600012);
117+
}
118+
119+
@Test
120+
public void testDifferentIdsWithDifferentRanges() {
121+
Jazzer.minimize(50, 0, 100, 1024, 600013);
122+
Jazzer.minimize(500, 0, 1000, 512, 600014);
123+
}
124+
125+
@Test
126+
public void testInconsistentNumCountersThrows() {
127+
// Same id and range but different numCounters (where range doesn't cap)
128+
Jazzer.minimize(50, 0, 10000, 1024, 600017);
129+
try {
130+
Jazzer.minimize(50, 0, 10000, 2048, 600017);
131+
fail("Expected JazzerApiException for inconsistent numCounters");
132+
} catch (JazzerApiException e) {
133+
assertTrue(
134+
e.getMessage().contains("numCounters")
135+
&& e.getMessage().contains("must remain constant"));
136+
}
137+
}
138+
139+
@Test
140+
public void testInconsistentMinValueThrows() {
141+
Jazzer.minimize(50, 0, 100, 1024, 600015);
142+
try {
143+
Jazzer.minimize(50, 10, 100, 1024, 600015);
144+
fail("Expected JazzerApiException for inconsistent minValue");
145+
} catch (JazzerApiException e) {
146+
assertTrue(e.getMessage().contains("must remain constant"));
147+
}
148+
}
149+
150+
@Test
151+
public void testInconsistentMaxValueThrows() {
152+
Jazzer.minimize(50, 0, 100, 1024, 600016);
153+
try {
154+
Jazzer.minimize(50, 0, 200, 1024, 600016);
155+
fail("Expected JazzerApiException for inconsistent maxValue");
156+
} catch (JazzerApiException e) {
157+
assertTrue(e.getMessage().contains("must remain constant"));
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)