diff --git a/examples/junit/src/test/java/com/example/BUILD.bazel b/examples/junit/src/test/java/com/example/BUILD.bazel index a4d119678..1d64d0a6c 100644 --- a/examples/junit/src/test/java/com/example/BUILD.bazel +++ b/examples/junit/src/test/java/com/example/BUILD.bazel @@ -372,3 +372,45 @@ 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", + ], +) + +# Test for the minimize() hill-climbing API. +# This test uses Jazzer.minimize() to guide the fuzzer toward minimizing +# a "temperature" value, demonstrating hill-climbing behavior in the opposite direction. +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..fb65e8e5d --- /dev/null +++ b/examples/junit/src/test/java/com/example/CoolerFuzzTest.java @@ -0,0 +1,68 @@ +/* + * 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. + // Map temperature in [0, 4000] to [0, 1023] + long mapped = temperature * 1023 / 4000; + Jazzer.minimize(mapped); + + if (temperature <= 100) { + throw new RuntimeException("Supercooled! Temperature minimized."); + } + } +} diff --git a/examples/junit/src/test/java/com/example/ReactorFuzzTest.java b/examples/junit/src/test/java/com/example/ReactorFuzzTest.java new file mode 100644 index 000000000..933130ead --- /dev/null +++ b/examples/junit/src/test/java/com/example/ReactorFuzzTest.java @@ -0,0 +1,60 @@ +/* + * 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). + long mapped = temperature * 1023 / 4500; + Jazzer.maximize(mapped); + if (temperature >= 4500) { + throw new RuntimeException("Meltdown! Temperature maximized."); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel index 377e598c9..aed86f6ee 100644 --- a/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -46,6 +46,7 @@ java_library( "FuzzerSecurityIssueMedium.java", "HookType.java", "Jazzer.java", + "JazzerApiException.java", "MethodHook.java", "MethodHooks.java", "//src/main/java/jaz", 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 2b882a269..6887ef038 100644 --- a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java +++ b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java @@ -33,6 +33,20 @@ 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; + + /** + * Fixed number of counters per hill-climbing call site. Users must map their domain values into + * [0, 1023] before calling the hill-climbing APIs: + * + *
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 @@ -119,8 +147,10 @@ public static void guideTowardsEquality(String current, String target, int id) { } try { TRACE_STRCMP.invokeExact(current, target, 1, id); + } catch (JazzerApiException e) { + throw e; } catch (Throwable e) { - e.printStackTrace(); + throw new JazzerApiException("guideTowardsEquality: " + e.getMessage(), e); } } @@ -142,8 +172,10 @@ public static void guideTowardsEquality(byte[] current, byte[] target, int id) { } try { TRACE_MEMCMP.invokeExact(current, target, 1, id); + } catch (JazzerApiException e) { + throw e; } catch (Throwable e) { - e.printStackTrace(); + throw new JazzerApiException("guideTowardsEquality: " + e.getMessage(), e); } } @@ -166,8 +198,10 @@ public static void guideTowardsContainment(String haystack, String needle, int i } try { TRACE_STRSTR.invokeExact(haystack, needle, id); + } catch (JazzerApiException e) { + throw e; } catch (Throwable e) { - e.printStackTrace(); + throw new JazzerApiException("guideTowardsContainment: " + e.getMessage(), e); } } @@ -212,8 +246,10 @@ public static void exploreState(byte state, int id) { int upperBits = id >>> 5; try { TRACE_PC_INDIR.invokeExact(upperBits, lowerBits); + } catch (JazzerApiException e) { + throw e; } catch (Throwable e) { - e.printStackTrace(); + throw new JazzerApiException("exploreState: " + e.getMessage(), e); } } @@ -230,6 +266,115 @@ 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 [0, 1023], provides + * feedback that all values in [0, v] are covered. + * + *
This enables corpus minimization to keep only the input resulting in the maximum value. + * Values below 0 provide no signal. Values above 1023 are clamped to 1023. + * + *
Each call site allocates exactly 1024 coverage counters. Map your domain values into [0, + * 1023] before calling this method: + * + *
{@code
+ * // Map temperature in [500, 4500] to [0, 1023]
+ * long mapped = (temperature - 500) * 1023 / (4500 - 500);
+ * Jazzer.maximize(mapped);
+ * }
+ *
+ * @param value The value to maximize (expected in [0, 1023]; negative values produce no signal,
+ * values above 1023 are clamped)
+ * @param id A unique identifier for this call site (must be consistent across runs)
+ */
+ public static void maximize(long value, int id) {
+ if (COUNTERS_TRACKER_ALLOCATE == null) {
+ return;
+ }
+
+ try {
+ COUNTERS_TRACKER_ALLOCATE.invokeExact(id, HILL_CLIMBING_RANGE);
+
+ if (value >= 0) {
+ int toOffset = (int) Math.min(value, HILL_CLIMBING_RANGE - 1);
+ COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
+ }
+ } catch (JazzerApiException e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new JazzerApiException("maximize: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Convenience overload of {@link #maximize(long, int)} that allows using automatically generated
+ * call-site identifiers. During instrumentation, calls to this method are replaced with calls to
+ * {@link #maximize(long, int)} using a unique id for each call site.
+ *
+ * Without instrumentation, this is a no-op. + * + * @param value The value to maximize (expected in [0, 1023]) + * @see #maximize(long, int) + */ + public static void maximize(long value) { + // Instrumentation replaces calls to this method with calls to maximize(long, int) + // using an automatically generated call-site id. Without instrumentation, this is a no-op. + } + + /** + * Hill-climbing API to minimize a value. For each observed value v in [0, 1023], provides + * feedback inversely proportional to the value: lower values set more counters. + * + *
This enables corpus minimization to keep only the input resulting in the minimum value. + * Values above 1023 provide no signal. Values below 0 are clamped to 0. + * + *
Each call site allocates exactly 1024 coverage counters. Map your domain values into [0, + * 1023] before calling this method: + * + *
{@code
+ * // Map temperature in [0, 4000] to [0, 1023]
+ * long mapped = temperature * 1023 / 4000;
+ * Jazzer.minimize(mapped);
+ * }
+ *
+ * @param value The value to minimize (expected in [0, 1023]; values above 1023 produce no signal,
+ * negative values are clamped to 0)
+ * @param id A unique identifier for this call site (must be consistent across runs)
+ */
+ public static void minimize(long value, int id) {
+ if (COUNTERS_TRACKER_ALLOCATE == null) {
+ return;
+ }
+
+ try {
+ COUNTERS_TRACKER_ALLOCATE.invokeExact(id, HILL_CLIMBING_RANGE);
+
+ if (value <= HILL_CLIMBING_RANGE - 1) {
+ // Inverse of maximize: lower value = more counters
+ int toOffset = HILL_CLIMBING_RANGE - 1 - (int) Math.max(value, 0);
+ 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, int)} that allows using automatically generated
+ * call-site identifiers. During instrumentation, calls to this method are replaced with calls to
+ * {@link #minimize(long, int)} using a unique id for each call site.
+ *
+ * Without instrumentation, this is a no-op. + * + * @param value The value to minimize (expected in [0, 1023]) + * @see #minimize(long, int) + */ + public static void minimize(long value) { + // Instrumentation replaces calls to this method with calls to minimize(long, int) + // using an automatically generated call-site id. Without instrumentation, this is a no-op. + } + /** * Make Jazzer report the provided {@link Throwable} as a finding. * @@ -261,8 +406,10 @@ public static void reportFindingFromHook(Throwable finding) { public static void onFuzzTargetReady(Runnable callback) { try { ON_FUZZ_TARGET_READY.invokeExact(callback); + } catch (JazzerApiException e) { + throw e; } catch (Throwable e) { - e.printStackTrace(); + throw new JazzerApiException("onFuzzTargetReady: " + e.getMessage(), e); } } diff --git a/src/main/java/com/code_intelligence/jazzer/api/JazzerApiException.java b/src/main/java/com/code_intelligence/jazzer/api/JazzerApiException.java new file mode 100644 index 000000000..25fa3a9af --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/JazzerApiException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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; + +/** + * Signals error from the Jazzer API (e.g. invalid arguments to {@link Jazzer#maximize}). + * + *
This exception is treated as a fatal error by the fuzzing engine rather than as a finding in
+ * the code under test. When thrown during fuzzing, it stops the current fuzz test with an error
+ * instead of reporting a bug in the fuzz target.
+ */
+public class JazzerApiException extends RuntimeException {
+ public JazzerApiException(String message) {
+ super(message);
+ }
+
+ public JazzerApiException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public JazzerApiException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
index 112c4a2df..99e6cc1d5 100644
--- a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
+++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java
@@ -16,6 +16,7 @@
package com.code_intelligence.jazzer.driver;
+import static com.code_intelligence.jazzer.driver.Constants.JAZZER_ERROR_EXIT_CODE;
import static com.code_intelligence.jazzer.driver.Constants.JAZZER_FINDING_EXIT_CODE;
import static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID;
import static java.lang.System.exit;
@@ -91,6 +92,8 @@ public final class FuzzTargetRunner {
private static final String OPENTEST4J_TEST_ABORTED_EXCEPTION =
"org.opentest4j.TestAbortedException";
+ private static final String JAZZER_API_EXCEPTION =
+ "com.code_intelligence.jazzer.api.JazzerApiException";
private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
@@ -271,6 +274,16 @@ private static int runOne(long dataPtr, int dataLength) {
finding = JazzerInternal.lastFinding;
JazzerInternal.lastFinding = null;
}
+ // JazzerApiException signals API error, not a finding in the code under test.
+ if (finding != null && finding.getClass().getName().equals(JAZZER_API_EXCEPTION)) {
+ Log.error("Jazzer API error", finding);
+ temporarilyDisableLibfuzzerExitHook();
+ if (fatalFindingHandlerForJUnit != null) {
+ fatalFindingHandlerForJUnit.accept(finding);
+ return LIBFUZZER_RETURN_FROM_DRIVER;
+ }
+ exit(JAZZER_ERROR_EXIT_CODE);
+ }
// Allow skipping invalid inputs in fuzz tests by using e.g. JUnit's assumeTrue.
if (finding == null || finding.getClass().getName().equals(OPENTEST4J_TEST_ABORTED_EXCEPTION)) {
return LIBFUZZER_CONTINUE;
diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java
index 49294ad8c..2e0275182 100644
--- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java
+++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java
@@ -52,6 +52,8 @@
import org.junit.platform.commons.support.AnnotationSupport;
class FuzzTestExecutor {
+ private static final String JAZZER_API_EXCEPTION =
+ "com.code_intelligence.jazzer.api.JazzerApiException";
private static final AtomicBoolean hasBeenPrepared = new AtomicBoolean();
private static final AtomicBoolean agentInstalled = new AtomicBoolean(false);
@@ -332,6 +334,9 @@ public Optional This class provides a flexible API for any consumer wanting to translate program state signals
+ * to coverage counters, enabling incremental progress feedback to the fuzzer. Use cases include:
+ *
+ * Each counter is a byte (0-255). Each ID has a range of counters accessible via indexes [0,
+ * numCounters - 1]. Allocation is explicit - call {@link #ensureCountersAllocated} first, then use
+ * the set methods.
+ *
+ * The counters are allocated from a dedicated memory region separate from the main coverage map,
+ * ensuring isolation and preventing interference with regular coverage tracking.
+ */
+public final class CountersTracker {
+ static {
+ RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
+ }
+
+ private static final String ENV_MAX_COUNTERS = "JAZZER_EXTRA_COUNTERS_MAX";
+
+ private static final int DEFAULT_MAX_COUNTERS = 1 << 18;
+
+ /** Maximum number of counters available (default 1M, configurable via environment variable). */
+ private static final int MAX_COUNTERS = initMaxCounters();
+
+ private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+
+ /** Base address of the counter memory region. */
+ private static final long countersAddress = UNSAFE.allocateMemory(MAX_COUNTERS);
+
+ /** Map from ID to allocated counter range. */
+ private static final ConcurrentHashMap Idempotent: if already allocated, validates that numCounters matches.
+ *
+ * @param id Unique identifier for this counter range
+ * @param numCounters Number of counters to allocate
+ * @throws IllegalArgumentException if called with different numCounters for same ID
+ * @throws IllegalStateException if counter space is exhausted
+ */
+ public static void ensureCountersAllocated(int id, int numCounters) {
+ if (numCounters <= 0) {
+ throw new IllegalArgumentException("numCounters must be positive, got: " + numCounters);
+ }
+
+ CounterRange range =
+ idToRange.computeIfAbsent(
+ id,
+ key -> {
+ int startOffset = nextOffset.getAndAdd(numCounters);
+ if (startOffset > MAX_COUNTERS - numCounters) {
+ throw new IllegalStateException(
+ String.format(
+ "Counter space exhausted: requested %d counters at offset %d, "
+ + "but only %d total counters available. "
+ + "Increase via %s environment variable or use smaller ranges.",
+ numCounters, startOffset, MAX_COUNTERS, ENV_MAX_COUNTERS));
+ }
+ int endOffset = startOffset + numCounters;
+
+ CounterRange newRange = new CounterRange(startOffset, numCounters);
+
+ // Register the new counters with libFuzzer
+ registerCounters(startOffset, endOffset);
+
+ return newRange;
+ });
+
+ // Validate numCounters matches (for calls with same ID but different numCounters)
+ if (range.numCounters != numCounters) {
+ throw new IllegalArgumentException(
+ String.format(
+ "ensureCountersAllocated() called with different numCounters for id %d: "
+ + "existing=%d, requested=%d",
+ id, range.numCounters, numCounters));
+ }
+ }
+
+ /**
+ * Helper to get range for an allocated ID, throws if not allocated.
+ *
+ * @param id The ID to look up
+ * @return The CounterRange for this ID
+ * @throws IllegalStateException if no counters allocated for this ID
+ */
+ private static CounterRange getRange(int id) {
+ CounterRange range = idToRange.get(id);
+ if (range == null) {
+ throw new IllegalStateException("No counters allocated for id: " + id);
+ }
+ return range;
+ }
+
+ /**
+ * Sets the value of a specific counter within a range.
+ *
+ * @param id The ID of the allocated counter range
+ * @param offset Offset within the range [0, numCounters)
+ * @param value The value to set (0-255)
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if offset is out of bounds
+ */
+ public static void setCounter(int id, int offset, byte value) {
+ CounterRange range = getRange(id);
+ if (offset < 0 || offset >= range.numCounters) {
+ throw new IndexOutOfBoundsException(
+ String.format(
+ "Counter offset %d out of bounds for range with %d counters",
+ offset, range.numCounters));
+ }
+ long address = countersAddress + range.startOffset + offset;
+ UNSAFE.putByte(address, value);
+ }
+
+ /**
+ * Sets the first counter (offset = 0) to the given value.
+ *
+ * @param id The ID of the allocated counter range
+ * @param value The value to set (0-255)
+ * @throws IllegalStateException if no counters allocated for this ID
+ */
+ public static void setCounter(int id, byte value) {
+ setCounter(id, 0, value);
+ }
+
+ /**
+ * Sets the first counter (offset = 0) to 1.
+ *
+ * @param id The ID of the allocated counter range
+ * @throws IllegalStateException if no counters allocated for this ID
+ */
+ public static void setCounter(int id) {
+ setCounter(id, 0, (byte) 1);
+ }
+
+ /**
+ * Sets multiple consecutive counters to a value.
+ *
+ * Efficient for setting ranges (e.g., all counters from 0 to N for hill-climbing).
+ *
+ * @param id The ID of the allocated counter range
+ * @param fromOffset Start offset (inclusive)
+ * @param toOffset End offset (inclusive)
+ * @param value The value to set
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if offsets are out of bounds
+ */
+ public static void setCounterRange(int id, int fromOffset, int toOffset, byte value) {
+ CounterRange range = getRange(id);
+ if (fromOffset < 0) {
+ throw new IndexOutOfBoundsException("fromOffset must be non-negative, got: " + fromOffset);
+ }
+ if (toOffset >= range.numCounters) {
+ throw new IndexOutOfBoundsException(
+ String.format(
+ "toOffset %d out of bounds for range with %d counters", toOffset, range.numCounters));
+ }
+ if (fromOffset > toOffset) {
+ throw new IllegalArgumentException(
+ String.format(
+ "fromOffset (%d) must not be greater than toOffset (%d)", fromOffset, toOffset));
+ }
+
+ long startAddress = countersAddress + range.startOffset + fromOffset;
+ int length = toOffset - fromOffset + 1;
+ UNSAFE.setMemory(startAddress, length, value);
+ }
+
+ /**
+ * Sets counters from offset 0 to toOffset (inclusive) to the given value.
+ *
+ * @param id The ID of the allocated counter range
+ * @param toOffset End offset (inclusive)
+ * @param value The value to set
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if toOffset is out of bounds
+ */
+ public static void setCounterRange(int id, int toOffset, byte value) {
+ setCounterRange(id, 0, toOffset, value);
+ }
+
+ /**
+ * Sets counters from offset 0 to toOffset (inclusive) to 1.
+ *
+ * Ideal for hill-climbing/maximize patterns where you want to signal progress up to a point.
+ *
+ * @param id The ID of the allocated counter range
+ * @param toOffset End offset (inclusive)
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if toOffset is out of bounds
+ */
+ public static void setCounterRange(int id, int toOffset) {
+ setCounterRange(id, 0, toOffset, (byte) 1);
+ }
+
+ /** Internal record of an allocated counter range. */
+ private static final class CounterRange {
+ final int startOffset;
+ final int numCounters;
+
+ CounterRange(int startOffset, int numCounters) {
+ this.startOffset = startOffset;
+ this.numCounters = numCounters;
+ }
+ }
+
+ private static int initMaxCounters() {
+ String value = System.getenv(ENV_MAX_COUNTERS);
+ if (value == null || value.isEmpty()) {
+ return DEFAULT_MAX_COUNTERS;
+ }
+ try {
+ int parsed = Integer.parseInt(value.trim());
+ if (parsed < 0) {
+ throw new IllegalArgumentException(
+ ENV_MAX_COUNTERS + " must not be negative, got: " + parsed);
+ }
+ return parsed;
+ } catch (NumberFormatException e) {
+ return DEFAULT_MAX_COUNTERS;
+ }
+ }
+
+ // Native methods
+
+ /**
+ * Initializes the native counter tracker with the base address of the counter region.
+ *
+ * @param countersAddress The base address of the counter memory region
+ */
+ private static native void initialize(long countersAddress);
+
+ /**
+ * Registers a range of counters with libFuzzer.
+ *
+ * @param startOffset Start offset of the range to register
+ * @param endOffset End offset (exclusive) of the range to register
+ */
+ private static native void registerCounters(int startOffset, int endOffset);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java
index c4442b403..5f7f8150c 100644
--- a/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java
@@ -39,10 +39,9 @@ public final class CoverageMap {
private static final String ENV_MAX_NUM_COUNTERS = "JAZZER_MAX_NUM_COUNTERS";
- private static final int MAX_NUM_COUNTERS =
- System.getenv(ENV_MAX_NUM_COUNTERS) != null
- ? Integer.parseInt(System.getenv(ENV_MAX_NUM_COUNTERS))
- : 1 << 20;
+ private static final int DEFAULT_MAX_NUM_COUNTERS = 1 << 20;
+
+ private static final int MAX_NUM_COUNTERS = initMaxNumCounters();
private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
private static final Class> LOG;
@@ -174,4 +173,21 @@ private static void logError(String message, Throwable t) {
private static native void initialize(long countersAddress);
private static native void registerNewCounters(int oldNumCounters, int newNumCounters);
+
+ private static int initMaxNumCounters() {
+ String value = System.getenv(ENV_MAX_NUM_COUNTERS);
+ if (value == null || value.isEmpty()) {
+ return DEFAULT_MAX_NUM_COUNTERS;
+ }
+ try {
+ int parsed = Integer.parseInt(value.trim());
+ if (parsed < 0) {
+ throw new IllegalArgumentException(
+ ENV_MAX_NUM_COUNTERS + " must not be negative, got: " + parsed);
+ }
+ return parsed;
+ } catch (NumberFormatException e) {
+ return DEFAULT_MAX_NUM_COUNTERS;
+ }
+ }
}
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 d1d9c1c01..8503f1af9 100644
--- a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java
@@ -43,4 +43,38 @@ public static void exploreStateWithId(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
Jazzer.exploreState((byte) arguments[0], hookId);
}
+
+ /**
+ * Replaces calls to {@link Jazzer#maximize(long)} with calls to {@link Jazzer#maximize(long,
+ * int)} using the hook id as the id parameter.
+ *
+ * This allows each call site to be tracked separately without requiring the user to manually
+ * provide a unique id.
+ */
+ @MethodHook(
+ type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.api.Jazzer",
+ targetMethod = "maximize",
+ targetMethodDescriptor = "(J)V")
+ public static void maximizeWithId(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ Jazzer.maximize((long) arguments[0], hookId);
+ }
+
+ /**
+ * Replaces calls to {@link Jazzer#minimize(long)} with calls to {@link Jazzer#minimize(long,
+ * int)} using the hook id as the id parameter.
+ *
+ * This allows each call site to be tracked separately without requiring the user to manually
+ * provide a unique id.
+ */
+ @MethodHook(
+ type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.api.Jazzer",
+ targetMethod = "minimize",
+ targetMethodDescriptor = "(J)V")
+ public static void minimizeWithId(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ Jazzer.minimize((long) arguments[0], hookId);
+ }
}
diff --git a/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel b/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel
index 4f2deef0e..ed7c5400a 100644
--- a/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel
+++ b/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel
@@ -25,7 +25,7 @@ cc_library(
name = "jazzer_driver_lib",
visibility = ["//src/test/native/com/code_intelligence/jazzer/driver/mocks:__pkg__"],
deps = [
- ":coverage_tracker",
+ ":counters_tracker",
":fuzz_target_runner",
":jazzer_fuzzer_callbacks",
":libfuzzer_callbacks",
@@ -45,10 +45,13 @@ cc_jni_library(
)
cc_library(
- name = "coverage_tracker",
- srcs = ["coverage_tracker.cpp"],
- hdrs = ["coverage_tracker.h"],
- deps = ["//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map.hdrs"],
+ name = "counters_tracker",
+ srcs = ["counters_tracker.cpp"],
+ hdrs = ["counters_tracker.h"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/runtime:counters_tracker.hdrs",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map.hdrs",
+ ],
# Symbols are only referenced dynamically via JNI.
alwayslink = True,
)
diff --git a/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp
new file mode 100644
index 000000000..ce556fd60
--- /dev/null
+++ b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp
@@ -0,0 +1,178 @@
+// Copyright 2024 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.
+
+#include "counters_tracker.h"
+
+#include
+ *
+ *
+ *