Skip to content

Commit f932854

Browse files
committed
feat: add ExtraCountersTracker for generic coverage counter management
Add ExtraCountersTracker, a Java/C++ component that manages dynamically allocated coverage counters separate from the main coverage map. This enables user-facing APIs (like maximize) to register synthetic coverage counters that are tracked by libFuzzer. Features: - Thread-safe counter allocation with per-id tracking - JNI bridge to register counters with libFuzzer's 8-bit counter API - Configurable max counter limit via JAZZER_EXTRA_COUNTERS_MAX env var - Convenience overloads for setCounter and setCounterRange
1 parent b097419 commit f932854

8 files changed

Lines changed: 786 additions & 133 deletions

File tree

src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ java_library(
100100

101101
# The following targets must only be referenced directly by tests or native implementations.
102102

103+
java_jni_library(
104+
name = "extra_counters_tracker",
105+
srcs = ["ExtraCountersTracker.java"],
106+
native_libs = select({
107+
"@platforms//os:android": ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"],
108+
"//conditions:default": [],
109+
}),
110+
visibility = [
111+
"//src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
112+
"//src/test:__subpackages__",
113+
],
114+
deps = [
115+
"//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
116+
],
117+
)
118+
103119
java_jni_library(
104120
name = "coverage_map",
105121
srcs = ["CoverageMap.java"],
@@ -171,6 +187,7 @@ java_library(
171187
deps = [
172188
":constants",
173189
":coverage_map",
190+
":extra_counters_tracker",
174191
":trace_data_flow_native_callbacks",
175192
"//src/main/java/com/code_intelligence/jazzer/api:hooks",
176193
],
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
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.runtime;
18+
19+
import com.code_intelligence.jazzer.utils.UnsafeProvider;
20+
import com.github.fmeum.rules_jni.RulesJni;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.concurrent.atomic.AtomicInteger;
23+
import sun.misc.Unsafe;
24+
25+
/**
26+
* Generic foundation for mapping program state to coverage counters.
27+
*
28+
* <p>This class provides a flexible API for any consumer wanting to translate program state signals
29+
* to coverage counters, enabling incremental progress feedback to the fuzzer. Use cases include:
30+
*
31+
* <ul>
32+
* <li>Hill-climbing (maximize API)
33+
* <li>State exploration
34+
* <li>Custom progress signals
35+
* </ul>
36+
*
37+
* <p>Each counter is a byte (0-255). Each ID has a range of counters accessible via indexes [0,
38+
* numCounters - 1]. Allocation is explicit - call {@link #ensureCountersAllocated} first, then use
39+
* the set methods.
40+
*
41+
* <p>The counters are allocated from a dedicated memory region separate from the main coverage map,
42+
* ensuring isolation and preventing interference with regular coverage tracking.
43+
*/
44+
public final class ExtraCountersTracker {
45+
static {
46+
RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
47+
}
48+
49+
private static final String ENV_MAX_COUNTERS = "JAZZER_EXTRA_COUNTERS_MAX";
50+
51+
private static final int DEFAULT_MAX_COUNTERS = 1 << 18;
52+
53+
/** Maximum number of counters available (default 256K, configurable via environment variable). */
54+
private static final int MAX_COUNTERS = initMaxCounters();
55+
56+
private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
57+
58+
/** Base address of the counter memory region. */
59+
private static final long countersAddress = UNSAFE.allocateMemory(MAX_COUNTERS);
60+
61+
/** Map from ID to allocated counter range. */
62+
private static final ConcurrentHashMap<Integer, CounterRange> idToRange =
63+
new ConcurrentHashMap<>();
64+
65+
/** Next available offset for counter allocation. */
66+
private static final AtomicInteger nextOffset = new AtomicInteger(0);
67+
68+
static {
69+
// Zero-initialize the counter region
70+
UNSAFE.setMemory(countersAddress, MAX_COUNTERS, (byte) 0);
71+
// Initialize native side (like CoverageMap does)
72+
initialize(countersAddress);
73+
}
74+
75+
private ExtraCountersTracker() {}
76+
77+
/**
78+
* Allocates a range of counters for the given ID.
79+
*
80+
* <p>Idempotent: if already allocated, validates that numCounters matches.
81+
*
82+
* @param id Unique identifier for this counter range
83+
* @param numCounters Number of counters to allocate
84+
* @throws IllegalArgumentException if called with different numCounters for same ID
85+
* @throws IllegalStateException if counter space is exhausted
86+
*/
87+
public static void ensureCountersAllocated(int id, int numCounters) {
88+
if (numCounters <= 0) {
89+
throw new IllegalArgumentException("numCounters must be positive, got: " + numCounters);
90+
}
91+
92+
CounterRange range =
93+
idToRange.computeIfAbsent(
94+
id,
95+
key -> {
96+
int startOffset = nextOffset.getAndAdd(numCounters);
97+
if (startOffset > MAX_COUNTERS - numCounters) {
98+
throw new IllegalStateException(
99+
String.format(
100+
"Counter space exhausted: requested %d counters at offset %d, "
101+
+ "but only %d total counters available. "
102+
+ "Increase via %s environment variable or use smaller ranges.",
103+
numCounters, startOffset, MAX_COUNTERS, ENV_MAX_COUNTERS));
104+
}
105+
int endOffset = startOffset + numCounters;
106+
107+
CounterRange newRange = new CounterRange(startOffset, numCounters);
108+
109+
// Register the new counters with libFuzzer
110+
registerCounters(startOffset, endOffset);
111+
112+
return newRange;
113+
});
114+
115+
// Validate numCounters matches (for calls with same ID but different numCounters)
116+
if (range.numCounters != numCounters) {
117+
throw new IllegalArgumentException(
118+
String.format(
119+
"numCounters for id %d must remain constant. Expected %d, got %d.",
120+
id, range.numCounters, numCounters));
121+
}
122+
}
123+
124+
/**
125+
* Helper to get range for an allocated ID, throws if not allocated.
126+
*
127+
* @param id The ID to look up
128+
* @return The CounterRange for this ID
129+
* @throws IllegalStateException if no counters allocated for this ID
130+
*/
131+
private static CounterRange getRange(int id) {
132+
CounterRange range = idToRange.get(id);
133+
if (range == null) {
134+
throw new IllegalStateException("No counters allocated for id: " + id);
135+
}
136+
return range;
137+
}
138+
139+
/**
140+
* Sets the value of a specific counter within a range.
141+
*
142+
* @param id The ID of the allocated counter range
143+
* @param offset Offset within the range [0, numCounters)
144+
* @param value The value to set (0-255)
145+
* @throws IllegalStateException if no counters allocated for this ID
146+
* @throws IndexOutOfBoundsException if offset is out of bounds
147+
*/
148+
public static void setCounter(int id, int offset, byte value) {
149+
CounterRange range = getRange(id);
150+
if (offset < 0 || offset >= range.numCounters) {
151+
throw new IndexOutOfBoundsException(
152+
String.format(
153+
"Counter offset %d out of bounds for range with %d counters",
154+
offset, range.numCounters));
155+
}
156+
long address = countersAddress + range.startOffset + offset;
157+
UNSAFE.putByte(address, value);
158+
}
159+
160+
/**
161+
* Sets the first counter (offset = 0) to the given value.
162+
*
163+
* @param id The ID of the allocated counter range
164+
* @param value The value to set (0-255)
165+
* @throws IllegalStateException if no counters allocated for this ID
166+
*/
167+
public static void setCounter(int id, byte value) {
168+
setCounter(id, 0, value);
169+
}
170+
171+
/**
172+
* Sets the first counter (offset = 0) to 1.
173+
*
174+
* @param id The ID of the allocated counter range
175+
* @throws IllegalStateException if no counters allocated for this ID
176+
*/
177+
public static void setCounter(int id) {
178+
setCounter(id, 0, (byte) 1);
179+
}
180+
181+
/**
182+
* Sets multiple consecutive counters to a value.
183+
*
184+
* <p>Efficient for setting ranges (e.g., all counters from 0 to N for hill-climbing).
185+
*
186+
* @param id The ID of the allocated counter range
187+
* @param fromOffset Start offset (inclusive)
188+
* @param toOffset End offset (inclusive)
189+
* @param value The value to set
190+
* @throws IllegalStateException if no counters allocated for this ID
191+
* @throws IndexOutOfBoundsException if offsets are out of bounds
192+
*/
193+
public static void setCounterRange(int id, int fromOffset, int toOffset, byte value) {
194+
CounterRange range = getRange(id);
195+
if (fromOffset < 0) {
196+
throw new IndexOutOfBoundsException("fromOffset must be non-negative, got: " + fromOffset);
197+
}
198+
if (toOffset >= range.numCounters) {
199+
throw new IndexOutOfBoundsException(
200+
String.format(
201+
"toOffset %d out of bounds for range with %d counters", toOffset, range.numCounters));
202+
}
203+
if (fromOffset > toOffset) {
204+
throw new IllegalArgumentException(
205+
String.format(
206+
"fromOffset (%d) must not be greater than toOffset (%d)", fromOffset, toOffset));
207+
}
208+
209+
long startAddress = countersAddress + range.startOffset + fromOffset;
210+
int length = toOffset - fromOffset + 1;
211+
UNSAFE.setMemory(startAddress, length, value);
212+
}
213+
214+
/**
215+
* Sets counters from offset 0 to toOffset (inclusive) to the given value.
216+
*
217+
* @param id The ID of the allocated counter range
218+
* @param toOffset End offset (inclusive)
219+
* @param value The value to set
220+
* @throws IllegalStateException if no counters allocated for this ID
221+
* @throws IndexOutOfBoundsException if toOffset is out of bounds
222+
*/
223+
public static void setCounterRange(int id, int toOffset, byte value) {
224+
setCounterRange(id, 0, toOffset, value);
225+
}
226+
227+
/**
228+
* Sets counters from offset 0 to toOffset (inclusive) to 1.
229+
*
230+
* <p>Ideal for hill-climbing/maximize patterns where you want to signal progress up to a point.
231+
*
232+
* @param id The ID of the allocated counter range
233+
* @param toOffset End offset (inclusive)
234+
* @throws IllegalStateException if no counters allocated for this ID
235+
* @throws IndexOutOfBoundsException if toOffset is out of bounds
236+
*/
237+
public static void setCounterRange(int id, int toOffset) {
238+
setCounterRange(id, 0, toOffset, (byte) 1);
239+
}
240+
241+
/** Internal record of an allocated counter range. */
242+
private static final class CounterRange {
243+
final int startOffset;
244+
final int numCounters;
245+
246+
CounterRange(int startOffset, int numCounters) {
247+
this.startOffset = startOffset;
248+
this.numCounters = numCounters;
249+
}
250+
}
251+
252+
private static int initMaxCounters() {
253+
String value = System.getenv(ENV_MAX_COUNTERS);
254+
if (value == null || value.isEmpty()) {
255+
return DEFAULT_MAX_COUNTERS;
256+
}
257+
try {
258+
int parsed = Integer.parseInt(value.trim());
259+
if (parsed < 0) {
260+
throw new IllegalArgumentException(
261+
ENV_MAX_COUNTERS + " must not be negative, got: " + parsed);
262+
}
263+
return parsed;
264+
} catch (NumberFormatException e) {
265+
return DEFAULT_MAX_COUNTERS;
266+
}
267+
}
268+
269+
// Native methods
270+
271+
/**
272+
* Initializes the native counter tracker with the base address of the counter region.
273+
*
274+
* @param countersAddress The base address of the counter memory region
275+
*/
276+
private static native void initialize(long countersAddress);
277+
278+
/**
279+
* Registers a range of counters with libFuzzer.
280+
*
281+
* @param startOffset Start offset of the range to register
282+
* @param endOffset End offset (exclusive) of the range to register
283+
*/
284+
private static native void registerCounters(int startOffset, int endOffset);
285+
}

src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ cc_library(
2525
name = "jazzer_driver_lib",
2626
visibility = ["//src/test/native/com/code_intelligence/jazzer/driver/mocks:__pkg__"],
2727
deps = [
28-
":coverage_tracker",
28+
":counters_tracker",
2929
":fuzz_target_runner",
3030
":jazzer_fuzzer_callbacks",
3131
":libfuzzer_callbacks",
@@ -45,10 +45,13 @@ cc_jni_library(
4545
)
4646

4747
cc_library(
48-
name = "coverage_tracker",
49-
srcs = ["coverage_tracker.cpp"],
50-
hdrs = ["coverage_tracker.h"],
51-
deps = ["//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map.hdrs"],
48+
name = "counters_tracker",
49+
srcs = ["counters_tracker.cpp"],
50+
hdrs = ["counters_tracker.h"],
51+
deps = [
52+
"//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map.hdrs",
53+
"//src/main/java/com/code_intelligence/jazzer/runtime:extra_counters_tracker.hdrs",
54+
],
5255
# Symbols are only referenced dynamically via JNI.
5356
alwayslink = True,
5457
)

0 commit comments

Comments
 (0)