diff --git a/examples/junit/src/test/java/com/example/BUILD.bazel b/examples/junit/src/test/java/com/example/BUILD.bazel index a4d119678..09665600b 100644 --- a/examples/junit/src/test/java/com/example/BUILD.bazel +++ b/examples/junit/src/test/java/com/example/BUILD.bazel @@ -372,3 +372,24 @@ 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", + ], +) 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..1221d8511 --- /dev/null +++ b/examples/junit/src/test/java/com/example/ReactorFuzzTest.java @@ -0,0 +1,59 @@ +/* + * 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). + Jazzer.maximize(temperature, 500, 4500); + if (temperature >= 4500) { + throw new RuntimeException("Meltdown! Temperature maximized."); + } + } +} 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..44e5e5d64 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,9 @@ 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; + static { Class> jazzerInternal = null; MethodHandle onFuzzTargetReady = null; @@ -40,6 +43,8 @@ public final class Jazzer { MethodHandle traceStrstr = null; MethodHandle traceMemcmp = null; MethodHandle tracePcIndir = null; + MethodHandle countersTrackerAllocate = null; + MethodHandle countersTrackerSetRange = null; try { jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal"); MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class); @@ -70,6 +75,16 @@ public final class Jazzer { tracePcIndir = MethodHandles.publicLookup() .findStatic(traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType); + + Class> countersTracker = + Class.forName("com.code_intelligence.jazzer.runtime.CountersTracker"); + MethodType allocateType = MethodType.methodType(void.class, int.class, int.class); + countersTrackerAllocate = + MethodHandles.publicLookup() + .findStatic(countersTracker, "ensureCountersAllocated", allocateType); + MethodType setRangeType = MethodType.methodType(void.class, int.class, int.class); + countersTrackerSetRange = + MethodHandles.publicLookup().findStatic(countersTracker, "setCounterRange", setRangeType); } catch (ClassNotFoundException ignore) { // Not running in the context of the agent. This is fine as long as no methods are called on // this class. @@ -86,6 +101,8 @@ public final class Jazzer { TRACE_STRSTR = traceStrstr; TRACE_MEMCMP = traceMemcmp; TRACE_PC_INDIR = tracePcIndir; + COUNTERS_TRACKER_ALLOCATE = countersTrackerAllocate; + COUNTERS_TRACKER_SET_RANGE = countersTrackerSetRange; } private Jazzer() {} @@ -93,7 +110,7 @@ private Jazzer() {} /** * A 32-bit random number that hooks can use to make pseudo-random choices between multiple * possible mutations they could guide the fuzzer towards. Hooks must not base the decision - * whether or not to report a finding on this number as this will make findings non-reproducible. + * whether to report a finding on this number as this will make findings non-reproducible. * *
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 @@ -230,6 +247,75 @@ 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 [minValue, maxValue], + * provides feedback that all values in [minValue, v] are covered. + * + *
This enables corpus minimization to keep only the input resulting in the maximum value. + * Values below minValue provide no signal. Values above maxValue are clamped to maxValue. + * + *
Important: This allocates (maxValue - minValue + 1) coverage counters per unique ID. + * For large value ranges, use a mapping function to reduce the range: + * + *
{@code
+ * // Map [0, 1_000_000] to [0, 1000] steps
+ * long step = value < 0 ? 0 : Math.min(value / 1000, 1000);
+ * Jazzer.maximize(step, id, 0, 1000);
+ * }
+ *
+ * @param value The value to maximize (will be clamped to [minValue, maxValue])
+ * @param id A unique identifier for this call site (must be consistent across runs)
+ * @param minValue The minimum value in the range (inclusive)
+ * @param maxValue The maximum value in the range (inclusive)
+ */
+ public static void maximize(long value, int id, long minValue, long maxValue) {
+ if (COUNTERS_TRACKER_ALLOCATE == null) {
+ return;
+ }
+
+ if (maxValue < minValue) {
+ throw new IllegalArgumentException("maxValue must be >= minValue");
+ }
+ long range = maxValue - minValue;
+ if (range < 0 || range > (long) Integer.MAX_VALUE - 1) {
+ throw new IllegalArgumentException(
+ "Range too large: (maxValue - minValue + 1) must be <= Integer.MAX_VALUE");
+ }
+
+ int numCounters = (int) (range + 1);
+
+ try {
+ // Allocate counters (idempotent, validates numCounters > 0 and consistency)
+ COUNTERS_TRACKER_ALLOCATE.invokeExact(id, numCounters);
+
+ // Set counters if value provides signal
+ if (value >= minValue) {
+ int toOffset = (int) (Math.min(value, maxValue) - minValue);
+ COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Convenience overload of {@link #maximize(long, int, long, long)} that allows using
+ * automatically generated call-site identifiers. During instrumentation, calls to this method are
+ * replaced with calls to {@link #maximize(long, int, long, long)} using a unique id for each call
+ * site.
+ *
+ * Without instrumentation, this is a no-op. + * + * @param value The value to maximize + * @param minValue The minimum value in the range (inclusive) + * @param maxValue The maximum value in the range (inclusive) + * @see #maximize(long, int, long, long) + */ + public static void maximize(long value, long minValue, long maxValue) { + // Instrumentation replaces calls to this method with calls to maximize(long, int, long, long) + // using 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/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel index 2b5f2d0ef..3403dda36 100644 --- a/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -100,6 +100,22 @@ java_library( # The following targets must only be referenced directly by tests or native implementations. +java_jni_library( + name = "counters_tracker", + srcs = ["CountersTracker.java"], + native_libs = select({ + "@platforms//os:android": ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"], + "//conditions:default": [], + }), + visibility = [ + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//src/test:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + java_jni_library( name = "coverage_map", srcs = ["CoverageMap.java"], @@ -170,6 +186,7 @@ java_library( ], deps = [ ":constants", + ":counters_tracker", ":coverage_map", ":trace_data_flow_native_callbacks", "//src/main/java/com/code_intelligence/jazzer/api:hooks", diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/CountersTracker.java b/src/main/java/com/code_intelligence/jazzer/runtime/CountersTracker.java new file mode 100644 index 000000000..918022bed --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/CountersTracker.java @@ -0,0 +1,290 @@ +/* + * 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.runtime; + +import com.code_intelligence.jazzer.utils.UnsafeProvider; +import com.github.fmeum.rules_jni.RulesJni; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import sun.misc.Unsafe; + +/** + * Generic foundation for mapping program state to coverage counters. + * + *
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_MAXIMIZE_MAX_COUNTERS";
+
+ private static final int DEFAULT_MAX_COUNTERS = 1 << 20;
+
+ /** 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 -> {
+ // Allocate space - only runs once per ID
+ int startOffset;
+ int endOffset;
+ do {
+ startOffset = nextOffset.get();
+ 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));
+ }
+ endOffset = startOffset + numCounters;
+ } while (!nextOffset.compareAndSet(startOffset, endOffset));
+
+ 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) {
+ return DEFAULT_MAX_COUNTERS;
+ }
+ 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/JazzerApiHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java
index d1d9c1c01..b16790d26 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,21 @@ 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, long, long)} with calls to {@link
+ * Jazzer#maximize(long, int, long, long)} 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 = "(JJJ)V")
+ public static void maximizeWithId(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ Jazzer.maximize((long) arguments[0], hookId, (long) arguments[1], (long) arguments[2]);
+ }
}
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