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..38e34cdcc 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 (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"); + } + + if (COUNTERS_TRACKER_ALLOCATE == null) { + return; + } + + 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..49035b08f --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/CountersTracker.java @@ -0,0 +1,286 @@ +/* + * 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 idToRange = + new ConcurrentHashMap<>(); + + /** Next available offset for counter allocation. */ + private static final AtomicInteger nextOffset = new AtomicInteger(0); + + static { + // Zero-initialize the counter region + UNSAFE.setMemory(countersAddress, MAX_COUNTERS, (byte) 0); + // Initialize native side (like CoverageMap does) + initialize(countersAddress); + } + + private CountersTracker() {} + + /** + * Allocates a range of counters for the given ID. + * + *

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(); + endOffset = startOffset + numCounters; + if (endOffset > MAX_COUNTERS) { + 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)); + } + } 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 { + return Integer.parseInt(value.trim()); + } 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 +#include + +#include +#include + +#include "com_code_intelligence_jazzer_runtime_CountersTracker.h" +#include "com_code_intelligence_jazzer_runtime_CoverageMap.h" + +extern "C" void __sanitizer_cov_8bit_counters_init(uint8_t *start, + uint8_t *end); +extern "C" void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, + const uintptr_t *pcs_end); +extern "C" size_t __sanitizer_cov_get_observed_pcs(uintptr_t **pc_entries); + +namespace { +void AssertNoException(JNIEnv &env) { + if (env.ExceptionCheck()) { + env.ExceptionDescribe(); + std::cerr << "ERROR: Java exception occurred in CountersTracker JNI code" + << std::endl; + _Exit(1); + } +} +} // namespace + +namespace jazzer { + +uint8_t *CountersTracker::coverage_counters_ = nullptr; +uint8_t *CountersTracker::extra_counters_ = nullptr; +std::mutex CountersTracker::mutex_; + +void CountersTracker::RegisterCounterRange(uint8_t *start, uint8_t *end) { + if (start >= end) { + return; + } + + std::size_t num_counters = end - start; + + // libFuzzer requires an array containing the instruction addresses associated + // with the coverage counters. Since these may be synthetic counters (not + // associated with real code), we create PC entries with the flag set to 0 to + // indicate they are not real PCs. The PC value is set to the counter index + // for identification purposes. + PCTableEntry *pc_entries = new PCTableEntry[num_counters]; + for (std::size_t i = 0; i < num_counters; ++i) { + pc_entries[i] = {i, 0}; + } + + std::lock_guard lock(mutex_); + __sanitizer_cov_8bit_counters_init(start, end); + __sanitizer_cov_pcs_init( + reinterpret_cast(pc_entries), + reinterpret_cast(pc_entries + num_counters)); +} + +void CountersTracker::Initialize(JNIEnv &env, jlong counters) { + if (coverage_counters_ != nullptr) { + std::cerr << "ERROR: CountersTracker::Initialize must not be called more " + "than once" + << std::endl; + _Exit(1); + } + coverage_counters_ = + reinterpret_cast(static_cast(counters)); +} + +void CountersTracker::RegisterNewCounters(JNIEnv &env, jint old_num_counters, + jint new_num_counters) { + if (coverage_counters_ == nullptr) { + std::cerr + << "ERROR: CountersTracker::Initialize should have been called first" + << std::endl; + _Exit(1); + } + if (new_num_counters < old_num_counters) { + std::cerr + << "ERROR: new_num_counters must not be smaller than old_num_counters" + << std::endl; + _Exit(1); + } + RegisterCounterRange(coverage_counters_ + old_num_counters, + coverage_counters_ + new_num_counters); +} + +void CountersTracker::InitializeExtra(JNIEnv &env, jlong counters) { + if (extra_counters_ != nullptr) { + std::cerr + << "ERROR: CountersTracker::InitializeExtra must not be called more " + "than once" + << std::endl; + _Exit(1); + } + extra_counters_ = + reinterpret_cast(static_cast(counters)); +} + +void CountersTracker::RegisterExtraCounters(JNIEnv &env, jint start_offset, + jint end_offset) { + if (extra_counters_ == nullptr) { + std::cerr << "ERROR: CountersTracker::InitializeExtra should have been " + "called first" + << std::endl; + _Exit(1); + } + if (end_offset < start_offset) { + std::cerr << "ERROR: end_offset must not be smaller than start_offset" + << std::endl; + _Exit(1); + } + RegisterCounterRange(extra_counters_ + start_offset, + extra_counters_ + end_offset); +} + +} // namespace jazzer + +// JNI exports for CoverageMap + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_CoverageMap_initialize( + JNIEnv *env, jclass, jlong counters) { + ::jazzer::CountersTracker::Initialize(*env, counters); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_CoverageMap_registerNewCounters( + JNIEnv *env, jclass, jint old_num_counters, jint new_num_counters) { + ::jazzer::CountersTracker::RegisterNewCounters(*env, old_num_counters, + new_num_counters); +} + +[[maybe_unused]] jintArray +Java_com_code_1intelligence_jazzer_runtime_CoverageMap_getEverCoveredIds( + JNIEnv *env, jclass) { + uintptr_t *covered_pcs; + jint num_covered_pcs = __sanitizer_cov_get_observed_pcs(&covered_pcs); + std::vector covered_edge_ids(covered_pcs, + covered_pcs + num_covered_pcs); + delete[] covered_pcs; + + jintArray covered_edge_ids_jni = env->NewIntArray(num_covered_pcs); + AssertNoException(*env); + env->SetIntArrayRegion(covered_edge_ids_jni, 0, num_covered_pcs, + covered_edge_ids.data()); + AssertNoException(*env); + return covered_edge_ids_jni; +} + +// JNI exports for CountersTracker + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_CountersTracker_initialize( + JNIEnv *env, jclass, jlong counters) { + ::jazzer::CountersTracker::InitializeExtra(*env, counters); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_CountersTracker_registerCounters( + JNIEnv *env, jclass, jint start_offset, jint end_offset) { + ::jazzer::CountersTracker::RegisterExtraCounters(*env, start_offset, + end_offset); +} diff --git a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.h similarity index 50% rename from src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h rename to src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.h index 34631af8c..ba7a15eb0 100644 --- a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h +++ b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.h @@ -17,7 +17,7 @@ #include #include -#include +#include namespace jazzer { @@ -26,16 +26,34 @@ struct __attribute__((packed)) PCTableEntry { [[maybe_unused]] uintptr_t PC, PCFlags; }; -// CoverageTracker registers an array of 8-bit coverage counters with -// libFuzzer. The array is populated from Java using Unsafe. -class CoverageTracker { +// CountersTracker manages coverage counter arrays and registers them with +// libFuzzer. It handles two separate counter regions: +// - Coverage counters: for bytecode edge coverage (used by CoverageMap) +// - Extra counters: for user APIs like maximize() (used by +// CountersTracker.java) +class CountersTracker { private: - static uint8_t *counters_; - static PCTableEntry *pc_entries_; + static uint8_t *coverage_counters_; + static uint8_t *extra_counters_; + static std::mutex mutex_; + + // Shared helper to register a counter range with libFuzzer. + static void RegisterCounterRange(uint8_t *start, uint8_t *end); public: + // For CoverageMap: initialize coverage counters base address. static void Initialize(JNIEnv &env, jlong counters); + + // For CoverageMap: register new coverage counters with libFuzzer. static void RegisterNewCounters(JNIEnv &env, jint old_num_counters, jint new_num_counters); + + // For CountersTracker.java: initialize extra counters base address. + static void InitializeExtra(JNIEnv &env, jlong counters); + + // For CountersTracker.java: register extra counters with libFuzzer. + static void RegisterExtraCounters(JNIEnv &env, jint start_offset, + jint end_offset); }; + } // namespace jazzer diff --git a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp b/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp deleted file mode 100644 index 58a05f1a7..000000000 --- a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp +++ /dev/null @@ -1,122 +0,0 @@ -// 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 "coverage_tracker.h" - -#include -#include - -#include -#include - -#include "com_code_intelligence_jazzer_runtime_CoverageMap.h" - -extern "C" void __sanitizer_cov_8bit_counters_init(uint8_t *start, - uint8_t *end); -extern "C" void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, - const uintptr_t *pcs_end); -extern "C" size_t __sanitizer_cov_get_observed_pcs(uintptr_t **pc_entries); - -namespace { -void AssertNoException(JNIEnv &env) { - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - std::cerr << "ERROR: Java exception occurred in CoverageTracker JNI code" - << std::endl; - _Exit(1); - } -} -} // namespace - -namespace jazzer { - -uint8_t *CoverageTracker::counters_ = nullptr; -PCTableEntry *CoverageTracker::pc_entries_ = nullptr; - -void CoverageTracker::Initialize(JNIEnv &env, jlong counters) { - if (counters_ != nullptr) { - std::cerr << "ERROR: CoverageTracker::Initialize must not be called more " - "than once" - << std::endl; - _Exit(1); - } - counters_ = reinterpret_cast(static_cast(counters)); -} - -void CoverageTracker::RegisterNewCounters(JNIEnv &env, jint old_num_counters, - jint new_num_counters) { - if (counters_ == nullptr) { - std::cerr - << "ERROR: CoverageTracker::Initialize should have been called first" - << std::endl; - _Exit(1); - } - if (new_num_counters < old_num_counters) { - std::cerr - << "ERROR: new_num_counters must not be smaller than old_num_counters" - << std::endl; - _Exit(1); - } - if (new_num_counters == old_num_counters) { - return; - } - std::size_t diff_num_counters = new_num_counters - old_num_counters; - // libFuzzer requires an array containing the instruction addresses associated - // with the coverage counters registered above. This is required to report how - // many edges have been covered. However, libFuzzer only checks these - // addresses when the corresponding flag is set to 1. Therefore, it is safe to - // set the all PC entries to any value as long as the corresponding flag is - // set to zero. We set the value of each PC to the index of the corresponding - // edge ID. This facilitates finding the edge ID of each covered PC reported - // by libFuzzer. - pc_entries_ = new PCTableEntry[diff_num_counters]; - for (std::size_t i = 0; i < diff_num_counters; ++i) { - pc_entries_[i] = {i, 0}; - } - __sanitizer_cov_8bit_counters_init(counters_ + old_num_counters, - counters_ + new_num_counters); - __sanitizer_cov_pcs_init((uintptr_t *)(pc_entries_), - (uintptr_t *)(pc_entries_ + diff_num_counters)); -} -} // namespace jazzer - -[[maybe_unused]] void -Java_com_code_1intelligence_jazzer_runtime_CoverageMap_initialize( - JNIEnv *env, jclass, jlong counters) { - ::jazzer::CoverageTracker::Initialize(*env, counters); -} - -[[maybe_unused]] void -Java_com_code_1intelligence_jazzer_runtime_CoverageMap_registerNewCounters( - JNIEnv *env, jclass, jint old_num_counters, jint new_num_counters) { - ::jazzer::CoverageTracker::RegisterNewCounters(*env, old_num_counters, - new_num_counters); -} - -[[maybe_unused]] jintArray -Java_com_code_1intelligence_jazzer_runtime_CoverageMap_getEverCoveredIds( - JNIEnv *env, jclass) { - uintptr_t *covered_pcs; - jint num_covered_pcs = __sanitizer_cov_get_observed_pcs(&covered_pcs); - std::vector covered_edge_ids(covered_pcs, - covered_pcs + num_covered_pcs); - delete[] covered_pcs; - - jintArray covered_edge_ids_jni = env->NewIntArray(num_covered_pcs); - AssertNoException(*env); - env->SetIntArrayRegion(covered_edge_ids_jni, 0, num_covered_pcs, - covered_edge_ids.data()); - AssertNoException(*env); - return covered_edge_ids_jni; -} 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 86014c7bd..8d0c0be51 100644 --- a/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -1,3 +1,5 @@ +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") + java_test( name = "AutofuzzTest", size = "small", @@ -20,3 +22,22 @@ java_test( "@maven//:junit_junit", ], ) + +java_test( + name = "MaximizeTest", + size = "small", + srcs = [ + "MaximizeTest.java", + ], + target_compatible_with = SKIP_ON_WINDOWS, + test_class = "com.code_intelligence.jazzer.api.MaximizeTest", + runtime_deps = [ + # Needed for CountersTracker at runtime. + "//src/main/java/com/code_intelligence/jazzer/runtime:counters_tracker", + ], + deps = [ + "//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/MaximizeTest.java b/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java new file mode 100644 index 000000000..a99173abb --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java @@ -0,0 +1,101 @@ +/* + * 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 org.junit.Test; + +public class MaximizeTest { + + @Test + public void testMaximizeBasic() { + // Basic usage - should not throw + Jazzer.maximize(50, 5000, 0, 100); + } + + @Test + public void testMaximizeWithMinValue() { + // Value at minimum + Jazzer.maximize(0, 5001, 0, 100); + } + + @Test + public void testMaximizeWithMaxValue() { + // Value at maximum + Jazzer.maximize(100, 5002, 0, 100); + } + + @Test + public void testMaximizeBelowMinValue() { + // Value below minimum - should be a no-op (no signal) + Jazzer.maximize(-10, 5003, 0, 100); + } + + @Test + public void testMaximizeAboveMaxValue() { + // Value above maximum - should be clamped + Jazzer.maximize(200, 5004, 0, 100); + } + + @Test + public void testMaximizeNegativeRange() { + // Negative range + Jazzer.maximize(-50, 5005, -100, 0); + } + + @Test + public void testMaximizeSingleValueRange() { + // Range with single value + Jazzer.maximize(42, 5006, 42, 42); + } + + @Test + public void testMaximizeInvalidRange() { + // maxValue < minValue - without runtime this is a no-op, + // with runtime CountersTracker catches "numCounters must be positive" + Jazzer.maximize(50, 5007, 100, 0); + } + + @Test + public void testMaximizeSameRangeSucceeds() { + // Multiple calls with same id and range should succeed + Jazzer.maximize(25, 5009, 0, 100); + Jazzer.maximize(50, 5009, 0, 100); + Jazzer.maximize(75, 5009, 0, 100); + } + + @Test + public void testMaximizeLargeRange() { + // Extremely large range - without runtime this is a no-op, + // with runtime would fail in CountersTracker + Jazzer.maximize(0, 5010, Long.MIN_VALUE, Long.MAX_VALUE); + } + + @Test + public void testMaximizeDifferentIds() { + // Different IDs can have different ranges + Jazzer.maximize(50, 5011, 0, 100); + Jazzer.maximize(500, 5012, 0, 1000); + Jazzer.maximize(-5, 5013, -10, 10); + } + + @Test + public void testMaximizeNoOpOverload() { + // The overload without explicit ID is a no-op without instrumentation + // This should not throw + Jazzer.maximize(50, 0, 100); + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel index db8e507a9..8907926ff 100644 --- a/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -1,5 +1,18 @@ load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") +java_test( + name = "CountersTrackerTest", + srcs = [ + "CountersTrackerTest.java", + ], + target_compatible_with = SKIP_ON_WINDOWS, + deps = [ + "//src/main/java/com/code_intelligence/jazzer/runtime:counters_tracker", + "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + "@maven//:junit_junit", + ], +) + java_test( name = "TraceCmpHooksTest", srcs = [ diff --git a/src/test/java/com/code_intelligence/jazzer/runtime/CountersTrackerTest.java b/src/test/java/com/code_intelligence/jazzer/runtime/CountersTrackerTest.java new file mode 100644 index 000000000..28ae5b506 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/runtime/CountersTrackerTest.java @@ -0,0 +1,282 @@ +/* + * 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 static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; + +public class CountersTrackerTest { + + @Test + public void testEnsureCountersAllocated() { + // Use unique ID to avoid overlap with other tests + CountersTracker.ensureCountersAllocated(1000, 100); + + // Should not throw - idempotent with same numCounters + CountersTracker.ensureCountersAllocated(1000, 100); + } + + @Test + public void testEnsureCountersAllocatedDifferentNumCountersThrows() { + CountersTracker.ensureCountersAllocated(1001, 100); + + try { + CountersTracker.ensureCountersAllocated(1001, 200); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("different numCounters")); + } + } + + @Test + public void testEnsureCountersAllocatedInvalidNumCountersThrows() { + try { + CountersTracker.ensureCountersAllocated(1002, 0); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + + try { + CountersTracker.ensureCountersAllocated(1003, -1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + } + + @Test + public void testSetCounterFullForm() { + CountersTracker.ensureCountersAllocated(1004, 10); + + // Should not throw + CountersTracker.setCounter(1004, 0, (byte) 1); + CountersTracker.setCounter(1004, 5, (byte) 42); + CountersTracker.setCounter(1004, 9, (byte) 255); + } + + @Test + public void testSetCounterConvenienceOverloads() { + CountersTracker.ensureCountersAllocated(1005, 10); + + // setCounter(id, value) - offset = 0 + CountersTracker.setCounter(1005, (byte) 42); + + // setCounter(id) - offset = 0, value = 1 + CountersTracker.setCounter(1005); + } + + @Test + public void testSetCounterNotAllocatedThrows() { + try { + CountersTracker.setCounter(9999999, 0, (byte) 1); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("No counters allocated")); + } + } + + @Test + public void testSetCounterOutOfBoundsThrows() { + CountersTracker.ensureCountersAllocated(1006, 10); + + try { + CountersTracker.setCounter(1006, -1, (byte) 1); + fail("Expected IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + + try { + CountersTracker.setCounter(1006, 10, (byte) 1); + fail("Expected IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + } + + @Test + public void testSetCounterRangeFullForm() { + CountersTracker.ensureCountersAllocated(1007, 100); + + // Should not throw + CountersTracker.setCounterRange(1007, 0, 50, (byte) 1); + CountersTracker.setCounterRange(1007, 0, 99, (byte) 1); + CountersTracker.setCounterRange(1007, 50, 50, (byte) 1); + } + + @Test + public void testSetCounterRangeConvenienceOverloads() { + CountersTracker.ensureCountersAllocated(1008, 100); + + // setCounterRange(id, toOffset, value) - fromOffset = 0 + CountersTracker.setCounterRange(1008, 50, (byte) 42); + + // setCounterRange(id, toOffset) - fromOffset = 0, value = 1 + CountersTracker.setCounterRange(1008, 99); + } + + @Test + public void testSetCounterRangeEmptyThrows() { + CountersTracker.ensureCountersAllocated(1009, 100); + + try { + // Empty range (fromOffset > toOffset) should throw + CountersTracker.setCounterRange(1009, 50, 40, (byte) 1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("must not be greater than")); + } + } + + @Test + public void testSetCounterRangeNotAllocatedThrows() { + try { + CountersTracker.setCounterRange(9999998, 0, 5, (byte) 1); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("No counters allocated")); + } + } + + @Test + public void testSetCounterRangeOutOfBoundsThrows() { + CountersTracker.ensureCountersAllocated(1010, 10); + + try { + CountersTracker.setCounterRange(1010, -1, 5, (byte) 1); + fail("Expected IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + assertTrue(e.getMessage().contains("non-negative")); + } + + try { + CountersTracker.setCounterRange(1010, 0, 10, (byte) 1); + fail("Expected IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + assertTrue(e.getMessage().contains("out of bounds")); + } + } + + @Test + public void testConcurrentAllocation() throws InterruptedException { + final int numThreads = 10; + final int numAllocationsPerThread = 100; + final ExecutorService executor = Executors.newFixedThreadPool(numThreads); + final CountDownLatch startLatch = new CountDownLatch(1); + final CountDownLatch doneLatch = new CountDownLatch(numThreads); + final AtomicReference error = new AtomicReference<>(); + + for (int t = 0; t < numThreads; t++) { + final int threadId = t; + executor.submit( + () -> { + try { + startLatch.await(); + for (int i = 0; i < numAllocationsPerThread; i++) { + // Use a large base ID to avoid overlap with other tests + int id = 100000 + threadId * 200 + i; + CountersTracker.ensureCountersAllocated(id, 10); + // Also test that we can use the counters after allocation + CountersTracker.setCounter(id, 0, (byte) 1); + } + } catch (Throwable e) { + error.compareAndSet(null, e); + } finally { + doneLatch.countDown(); + } + }); + } + + // Start all threads at once + startLatch.countDown(); + + // Wait for completion + assertTrue("Threads didn't finish in time", doneLatch.await(30, TimeUnit.SECONDS)); + executor.shutdown(); + + if (error.get() != null) { + fail("Concurrent allocation failed: " + error.get().getMessage()); + } + } + + @Test + public void testConcurrentAllocationSameId() throws InterruptedException { + final int numThreads = 10; + final int sharedId = 200000; // Use unique ID to avoid overlap with other tests + final int numCounters = 50; + final ExecutorService executor = Executors.newFixedThreadPool(numThreads); + final CountDownLatch startLatch = new CountDownLatch(1); + final CountDownLatch doneLatch = new CountDownLatch(numThreads); + final AtomicReference error = new AtomicReference<>(); + + for (int t = 0; t < numThreads; t++) { + executor.submit( + () -> { + try { + startLatch.await(); + // All threads try to allocate the same ID - should be idempotent + CountersTracker.ensureCountersAllocated(sharedId, numCounters); + // All threads should be able to use the counters + CountersTracker.setCounterRange(sharedId, numCounters - 1); + } catch (Throwable e) { + error.compareAndSet(null, e); + } finally { + doneLatch.countDown(); + } + }); + } + + // Start all threads at once + startLatch.countDown(); + + // Wait for completion + assertTrue("Threads didn't finish in time", doneLatch.await(30, TimeUnit.SECONDS)); + executor.shutdown(); + + if (error.get() != null) { + fail("Concurrent allocation failed: " + error.get().getMessage()); + } + } + + @Test + public void testMaximizePattern() { + // Test the typical usage pattern for maximize() + int id = 400000; + int numCounters = 101; // For range [0, 100] + + CountersTracker.ensureCountersAllocated(id, numCounters); + + // Simulate maximize(50, id, 0, 100) - should set counters 0-50 to 1 + int value = 50; + int minValue = 0; + int steps = value - minValue; + CountersTracker.setCounterRange(id, steps); + + // Should be able to call again with higher value + value = 75; + steps = value - minValue; + CountersTracker.setCounterRange(id, steps); + } +}