From f9035703c1a4329cc04e54c3a4e607a2fdacd728 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Fri, 20 Feb 2026 18:06:24 +0100 Subject: [PATCH 1/2] feat: add minimize API --- .../code_intelligence/jazzer/api/Jazzer.java | 89 ++++++++++ .../jazzer/runtime/JazzerApiHooks.java | 35 ++++ .../code_intelligence/jazzer/api/BUILD.bazel | 19 +++ .../jazzer/api/MinimizeTest.java | 160 ++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 src/test/java/com/code_intelligence/jazzer/api/MinimizeTest.java diff --git a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java index 353f2d598..44d4dd075 100644 --- a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java +++ b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java @@ -380,6 +380,95 @@ public static void maximize(long value, long minValue, long maxValue, int numCou // Without instrumentation, this is a no-op. } + /** + * Core implementation of the hill-climbing minimize API. It maps {@code value} from the range + * [{@code minValue}, {@code maxValue}] onto {@code numCounters} coverage counters via inverse + * linear interpolation, then sets all counters from 0 to the mapped offset. + * + *

Lower values produce more signal (more counters set), which causes the fuzzer to prefer + * inputs that result in lower values. Values above {@code maxValue} produce no signal. Values + * below {@code minValue} are clamped. + * + *

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 minimize + * @param minValue the minimum expected value (inclusive) + * @param maxValue the maximum expected value (inclusive); must be >= {@code minValue} + * @param numCounters the number of counters to allocate; must be > 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 minimize(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 <= maxValue) { + int toOffset; + if (minValue == maxValue) { + toOffset = 0; + } else { + double range = (double) maxValue - (double) minValue; + double offset = (double) maxValue - (double) Math.max(value, 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("minimize: " + e.getMessage(), e); + } + } + + /** + * Convenience overload of {@link #minimize(long, long, long, int, int)} that uses {@link + * #DEFAULT_NUM_COUNTERS} counters and an automatically generated call-site id. + * + *

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. + * + *

{@code
+   * // Minimize temperature in [0, 4000]
+   * Jazzer.minimize(temperature, 0, 4000);
+   * }
+ * + * @param value the value to minimize + * @param minValue the minimum expected value (inclusive) + * @param maxValue the maximum expected value (inclusive) + * @see #minimize(long, long, long, int, int) + */ + public static void minimize(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 #minimize(long, long, long, int, int)} that uses a custom number + * of counters and an automatically generated call-site id. + * + *

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 minimize + * @param minValue the minimum expected value (inclusive) + * @param maxValue the maximum expected value (inclusive) + * @param numCounters the number of counters to allocate; must be > 0 + * @see #minimize(long, long, long, int, int) + */ + public static void minimize(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. * diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java index ddd740e8e..510e67ad5 100644 --- a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java +++ b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java @@ -78,4 +78,39 @@ public static void maximizeWithCustomCountersAndId( Jazzer.maximize( (long) arguments[0], (long) arguments[1], (long) arguments[2], (int) arguments[3], hookId); } + + /** + * Replaces calls to {@link Jazzer#minimize(long, long, long)} with calls to {@link + * Jazzer#minimize(long, long, long, int, int)} using {@link Jazzer#DEFAULT_NUM_COUNTERS} and the + * hook id. + */ + @MethodHook( + type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.api.Jazzer", + targetMethod = "minimize", + targetMethodDescriptor = "(JJJ)V") + public static void minimizeWithDefaultCountersAndId( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + Jazzer.minimize( + (long) arguments[0], + (long) arguments[1], + (long) arguments[2], + Jazzer.DEFAULT_NUM_COUNTERS, + hookId); + } + + /** + * Replaces calls to {@link Jazzer#minimize(long, long, long, int)} with calls to {@link + * Jazzer#minimize(long, long, long, int, int)} using the hook id. + */ + @MethodHook( + type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.api.Jazzer", + targetMethod = "minimize", + targetMethodDescriptor = "(JJJI)V") + public static void minimizeWithCustomCountersAndId( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + Jazzer.minimize( + (long) arguments[0], (long) arguments[1], (long) arguments[2], (int) arguments[3], hookId); + } } diff --git a/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel index b8f19e25a..a12a081e0 100644 --- a/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -41,3 +41,22 @@ java_test( "@maven//:junit_junit", ], ) + +java_test( + name = "MinimizeTest", + size = "small", + srcs = [ + "MinimizeTest.java", + ], + target_compatible_with = SKIP_ON_WINDOWS, + test_class = "com.code_intelligence.jazzer.api.MinimizeTest", + runtime_deps = [ + "//src/main/java/com/code_intelligence/jazzer/runtime", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + "@maven//:junit_junit", + ], +) diff --git a/src/test/java/com/code_intelligence/jazzer/api/MinimizeTest.java b/src/test/java/com/code_intelligence/jazzer/api/MinimizeTest.java new file mode 100644 index 000000000..d62bb1acc --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/api/MinimizeTest.java @@ -0,0 +1,160 @@ +/* + * 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; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +public class MinimizeTest { + + @Test + public void testBasicRangeMapping() { + // value=50 in [0, 100] with 1024 counters + // offset = (100 - 50) / 100 * 1023 = 511 + Jazzer.minimize(50, 0, 100, 1024, 600000); + } + + @Test + public void testValueAtMinimum() { + // value == minValue → offset = effectiveCounters - 1 (maximum signal) + Jazzer.minimize(0, 0, 100, 1024, 600001); + } + + @Test + public void testValueAtMaximum() { + // value == maxValue → offset = 0 (minimum signal) + Jazzer.minimize(100, 0, 100, 1024, 600002); + } + + @Test + public void testValueAboveMaximum() { + // value > maxValue → no signal (should not throw) + Jazzer.minimize(200, 0, 100, 1024, 600003); + } + + @Test + public void testValueBelowMinimum() { + // value < minValue → clamped to minValue (offset = effectiveCounters - 1) + Jazzer.minimize(-10, 0, 100, 1024, 600004); + } + + @Test + public void testNegativeRange() { + // Range with negative values: [-100, -50] + Jazzer.minimize(-75, -100, -50, 1024, 600005); + } + + @Test + public void testSingleValueRange() { + // minValue == maxValue → offset is always 0 + Jazzer.minimize(42, 42, 42, 1024, 600006); + } + + @Test + public void testLargeRange() { + // Long.MIN_VALUE to Long.MAX_VALUE — should not overflow + Jazzer.minimize(0, Long.MIN_VALUE, Long.MAX_VALUE, 1024, 600007); + } + + @Test + public void testCustomNumCounters() { + // Expert overload with small counter count + Jazzer.minimize(50, 0, 100, 10, 600008); + } + + @Test + public void testZeroNumCountersThrows() { + try { + Jazzer.minimize(50, 0, 100, 0, 600009); + fail("Expected JazzerApiException for zero numCounters"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + } + + @Test + public void testNegativeNumCountersThrows() { + try { + Jazzer.minimize(50, 0, 100, -5, 600010); + fail("Expected JazzerApiException for negative numCounters"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + } + + @Test + public void testMaxValueLessThanMinValueThrows() { + try { + Jazzer.minimize(50, 100, 0, 1024, 600011); + fail("Expected JazzerApiException for maxValue < minValue"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must not be less than")); + } + } + + @Test + public void testMultipleCallsSameId() { + // Multiple calls with the same id should succeed (idempotent allocation) + Jazzer.minimize(10, 0, 100, 1024, 600012); + Jazzer.minimize(50, 0, 100, 1024, 600012); + Jazzer.minimize(90, 0, 100, 1024, 600012); + } + + @Test + public void testDifferentIdsWithDifferentRanges() { + Jazzer.minimize(50, 0, 100, 1024, 600013); + Jazzer.minimize(500, 0, 1000, 512, 600014); + } + + @Test + public void testInconsistentNumCountersThrows() { + // Same id and range but different numCounters (where range doesn't cap) + Jazzer.minimize(50, 0, 10000, 1024, 600017); + try { + Jazzer.minimize(50, 0, 10000, 2048, 600017); + fail("Expected JazzerApiException for inconsistent numCounters"); + } catch (JazzerApiException e) { + assertTrue( + e.getMessage().contains("numCounters") + && e.getMessage().contains("must remain constant")); + } + } + + @Test + public void testInconsistentMinValueThrows() { + Jazzer.minimize(50, 0, 100, 1024, 600015); + try { + Jazzer.minimize(50, 10, 100, 1024, 600015); + fail("Expected JazzerApiException for inconsistent minValue"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must remain constant")); + } + } + + @Test + public void testInconsistentMaxValueThrows() { + Jazzer.minimize(50, 0, 100, 1024, 600016); + try { + Jazzer.minimize(50, 0, 200, 1024, 600016); + fail("Expected JazzerApiException for inconsistent maxValue"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must remain constant")); + } + } +} From 8d07b9c96486c0724371763c7d2ea1266552ab20 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Fri, 20 Feb 2026 18:09:22 +0100 Subject: [PATCH 2/2] feat: add fuzz test example for minimize --- .../src/test/java/com/example/BUILD.bazel | 21 ++++++ .../test/java/com/example/CoolerFuzzTest.java | 65 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 examples/junit/src/test/java/com/example/CoolerFuzzTest.java diff --git a/examples/junit/src/test/java/com/example/BUILD.bazel b/examples/junit/src/test/java/com/example/BUILD.bazel index 09665600b..d8145ebbc 100644 --- a/examples/junit/src/test/java/com/example/BUILD.bazel +++ b/examples/junit/src/test/java/com/example/BUILD.bazel @@ -393,3 +393,24 @@ java_fuzz_target_test( "@maven//:org_junit_jupiter_junit_jupiter_api", ], ) + +# Test for the minimize() hill-climbing API. +# This test uses Jazzer.minimize() to guide the fuzzer toward minimizing +# a "temperature" value, demonstrating inverse hill-climbing behavior. +java_fuzz_target_test( + name = "CoolerFuzzTest", + srcs = ["CoolerFuzzTest.java"], + allowed_findings = ["java.lang.RuntimeException"], + env = {"JAZZER_FUZZ": "1"}, + target_class = "com.example.CoolerFuzzTest", + 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", + ], +) diff --git a/examples/junit/src/test/java/com/example/CoolerFuzzTest.java b/examples/junit/src/test/java/com/example/CoolerFuzzTest.java new file mode 100644 index 000000000..48df89856 --- /dev/null +++ b/examples/junit/src/test/java/com/example/CoolerFuzzTest.java @@ -0,0 +1,65 @@ +/* + * 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; + +/** + * Example demonstrating the minimize() hill-climbing API. + * + *

Mirror of ReactorFuzzTest: instead of heating up a reactor, we're trying to cool down a system + * to the lowest possible temperature. + */ +public class CoolerFuzzTest { + + @FuzzTest + public void fuzz(@NotNull String input) { + for (char c : input.toCharArray()) { + if (c < 32 || c > 126) return; + } + controlCooler(input); + } + + private void controlCooler(String commands) { + long temperature = 4000; // Starts hot + + for (char cmd : commands.toCharArray()) { + // Complex, chaotic feedback loop. + // Hard to predict which character decreases temperature. + if ((temperature ^ cmd) % 3 == 0) { + temperature -= (cmd % 10); // Cool down slightly + } else if ((temperature ^ cmd) % 3 == 1) { + temperature += (cmd % 8); // Heat up slightly + } else { + temperature -= 1; // Tiny decrease + } + + // Cap at reasonable bounds + if (temperature < 0) temperature = 0; + if (temperature > 5000) temperature = 5000; + } + + // THE GOAL: MINIMIZATION + // Drive 'temperature' to the lowest possible value. + Jazzer.minimize(temperature, 0, 4000); + if (temperature <= 100) { + throw new RuntimeException("Supercooled! Temperature minimized."); + } + } +}