From 2bfc7fee351a61f5d9396897edfb74e497aba5e3 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Fri, 20 Feb 2026 08:26:45 +0100 Subject: [PATCH 1/4] fix: harden CoverageMap env var parsing and validation Properly parse JAZZER_MAX_NUM_COUNTERS with trimming, reject negative values with a clear error message, and cap INITIAL_NUM_COUNTERS at MAX_NUM_COUNTERS to prevent out-of-bounds registration. --- .../jazzer/runtime/CoverageMap.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) 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..b3eb60caa 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; @@ -82,7 +81,7 @@ public final class CoverageMap { private static final int INITIAL_NUM_COUNTERS = 1 << 9; static { - registerNewCounters(0, INITIAL_NUM_COUNTERS); + registerNewCounters(0, Math.min(INITIAL_NUM_COUNTERS, MAX_NUM_COUNTERS)); } /** @@ -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; + } + } } From 8f3b5a6119b5f4e5c77953c830027a6c1526dcd0 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Fri, 20 Feb 2026 08:27:02 +0100 Subject: [PATCH 2/4] 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 --- .../jazzer/runtime/BUILD.bazel | 17 ++ .../jazzer/runtime/ExtraCountersTracker.java | 285 ++++++++++++++++++ .../jazzer/driver/BUILD.bazel | 13 +- .../jazzer/driver/counters_tracker.cpp | 177 +++++++++++ ...{coverage_tracker.h => counters_tracker.h} | 30 +- .../jazzer/driver/coverage_tracker.cpp | 122 -------- .../jazzer/runtime/BUILD.bazel | 13 + .../runtime/ExtraCountersTrackerTest.java | 262 ++++++++++++++++ 8 files changed, 786 insertions(+), 133 deletions(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/runtime/ExtraCountersTracker.java create mode 100644 src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp rename src/main/native/com/code_intelligence/jazzer/driver/{coverage_tracker.h => counters_tracker.h} (50%) delete mode 100644 src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp create mode 100644 src/test/java/com/code_intelligence/jazzer/runtime/ExtraCountersTrackerTest.java 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..dcd90af08 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 = "extra_counters_tracker", + srcs = ["ExtraCountersTracker.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"], @@ -171,6 +187,7 @@ java_library( deps = [ ":constants", ":coverage_map", + ":extra_counters_tracker", ":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/ExtraCountersTracker.java b/src/main/java/com/code_intelligence/jazzer/runtime/ExtraCountersTracker.java new file mode 100644 index 000000000..b6a2b4ec2 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/ExtraCountersTracker.java @@ -0,0 +1,285 @@ +/* + * 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 ExtraCountersTracker { + 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 256K, 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 ExtraCountersTracker() {} + + /** + * 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 -> { + 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( + "numCounters for id %d must remain constant. Expected %d, got %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/native/com/code_intelligence/jazzer/driver/BUILD.bazel b/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel index 4f2deef0e..25e10d882 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:coverage_map.hdrs", + "//src/main/java/com/code_intelligence/jazzer/runtime:extra_counters_tracker.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..a43d0fb11 --- /dev/null +++ b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp @@ -0,0 +1,177 @@ +// 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_CoverageMap.h" +#include "com_code_intelligence_jazzer_runtime_ExtraCountersTracker.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 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 ExtraCountersTracker + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_ExtraCountersTracker_initialize( + JNIEnv *env, jclass, jlong counters) { + ::jazzer::CountersTracker::InitializeExtra(*env, counters); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_ExtraCountersTracker_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..999b7a13d 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 +// ExtraCountersTracker.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 ExtraCountersTracker.java: initialize extra counters base address. + static void InitializeExtra(JNIEnv &env, jlong counters); + + // For ExtraCountersTracker.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/runtime/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel index db8e507a9..c00d0bf65 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 = "ExtraCountersTrackerTest", + srcs = [ + "ExtraCountersTrackerTest.java", + ], + target_compatible_with = SKIP_ON_WINDOWS, + deps = [ + "//src/main/java/com/code_intelligence/jazzer/runtime:extra_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/ExtraCountersTrackerTest.java b/src/test/java/com/code_intelligence/jazzer/runtime/ExtraCountersTrackerTest.java new file mode 100644 index 000000000..c754e553a --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/runtime/ExtraCountersTrackerTest.java @@ -0,0 +1,262 @@ +/* + * 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 ExtraCountersTrackerTest { + + @Test + public void testEnsureCountersAllocated() { + // Use unique ID to avoid overlap with other tests + ExtraCountersTracker.ensureCountersAllocated(1000, 100); + + // Should not throw - idempotent with same numCounters + ExtraCountersTracker.ensureCountersAllocated(1000, 100); + } + + @Test + public void testEnsureCountersAllocatedDifferentNumCountersThrows() { + ExtraCountersTracker.ensureCountersAllocated(1001, 100); + + try { + ExtraCountersTracker.ensureCountersAllocated(1001, 200); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("must remain constant")); + } + } + + @Test + public void testEnsureCountersAllocatedInvalidNumCountersThrows() { + try { + ExtraCountersTracker.ensureCountersAllocated(1002, 0); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + + try { + ExtraCountersTracker.ensureCountersAllocated(1003, -1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + } + + @Test + public void testSetCounterFullForm() { + ExtraCountersTracker.ensureCountersAllocated(1004, 10); + + // Should not throw + ExtraCountersTracker.setCounter(1004, 0, (byte) 1); + ExtraCountersTracker.setCounter(1004, 5, (byte) 42); + ExtraCountersTracker.setCounter(1004, 9, (byte) 255); + } + + @Test + public void testSetCounterConvenienceOverloads() { + ExtraCountersTracker.ensureCountersAllocated(1005, 10); + + // setCounter(id, value) - offset = 0 + ExtraCountersTracker.setCounter(1005, (byte) 42); + + // setCounter(id) - offset = 0, value = 1 + ExtraCountersTracker.setCounter(1005); + } + + @Test + public void testSetCounterNotAllocatedThrows() { + try { + ExtraCountersTracker.setCounter(9999999, 0, (byte) 1); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("No counters allocated")); + } + } + + @Test + public void testSetCounterOutOfBoundsThrows() { + ExtraCountersTracker.ensureCountersAllocated(1006, 10); + + try { + ExtraCountersTracker.setCounter(1006, -1, (byte) 1); + fail("Expected IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + + try { + ExtraCountersTracker.setCounter(1006, 10, (byte) 1); + fail("Expected IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + } + + @Test + public void testSetCounterRangeFullForm() { + ExtraCountersTracker.ensureCountersAllocated(1007, 100); + + // Should not throw + ExtraCountersTracker.setCounterRange(1007, 0, 50, (byte) 1); + ExtraCountersTracker.setCounterRange(1007, 0, 99, (byte) 1); + ExtraCountersTracker.setCounterRange(1007, 50, 50, (byte) 1); + } + + @Test + public void testSetCounterRangeConvenienceOverloads() { + ExtraCountersTracker.ensureCountersAllocated(1008, 100); + + // setCounterRange(id, toOffset, value) - fromOffset = 0 + ExtraCountersTracker.setCounterRange(1008, 50, (byte) 42); + + // setCounterRange(id, toOffset) - fromOffset = 0, value = 1 + ExtraCountersTracker.setCounterRange(1008, 99); + } + + @Test + public void testSetCounterRangeEmptyThrows() { + ExtraCountersTracker.ensureCountersAllocated(1009, 100); + + try { + // Empty range (fromOffset > toOffset) should throw + ExtraCountersTracker.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 { + ExtraCountersTracker.setCounterRange(9999998, 0, 5, (byte) 1); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("No counters allocated")); + } + } + + @Test + public void testSetCounterRangeOutOfBoundsThrows() { + ExtraCountersTracker.ensureCountersAllocated(1010, 10); + + try { + ExtraCountersTracker.setCounterRange(1010, -1, 5, (byte) 1); + fail("Expected IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + assertTrue(e.getMessage().contains("non-negative")); + } + + try { + ExtraCountersTracker.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; + ExtraCountersTracker.ensureCountersAllocated(id, 10); + // Also test that we can use the counters after allocation + ExtraCountersTracker.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 + ExtraCountersTracker.ensureCountersAllocated(sharedId, numCounters); + // All threads should be able to use the counters + ExtraCountersTracker.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()); + } + } +} From af28d6d8fdbf8c03646e94d7734fa7192be0bcce Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Fri, 20 Feb 2026 08:27:11 +0100 Subject: [PATCH 3/4] feat: add JazzerApiException for API error handling Introduce JazzerApiException to distinguish API misuse errors (invalid arguments, incompatible runtime) from findings in the code under test. The fuzzing engine treats this exception as a fatal error rather than a reportable bug. --- .../code_intelligence/jazzer/api/BUILD.bazel | 1 + .../jazzer/api/JazzerApiException.java | 38 +++++++++++++++++++ .../jazzer/driver/FuzzTargetRunner.java | 13 +++++++ .../jazzer/junit/FuzzTestExecutor.java | 5 +++ .../jazzer/junit/FuzzTestExtensions.java | 6 +++ 5 files changed, 63 insertions(+) create mode 100644 src/main/java/com/code_intelligence/jazzer/api/JazzerApiException.java 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/JazzerApiException.java b/src/main/java/com/code_intelligence/jazzer/api/JazzerApiException.java new file mode 100644 index 000000000..bf84b676a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/api/JazzerApiException.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * 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 final 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 63b300114..6c7106d57 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 d88cfc325..a5fe9baff 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); @@ -334,6 +336,9 @@ public Optional execute( Throwable finding = atomicFinding.get(); if (finding != null) { + if (finding.getClass().getName().equals(JAZZER_API_EXCEPTION)) { + return Optional.of(finding); + } return Optional.of(new FuzzTestFindingException(finding)); } else if (exitCode != 0) { return Optional.of( diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java index fcc1d4186..37ccc90ee 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExtensions.java @@ -42,6 +42,8 @@ class FuzzTestExtensions implements ExecutionCondition, InvocationInterceptor, TestExecutionExceptionHandler { private static final String JAZZER_INTERNAL = "com.code_intelligence.jazzer.runtime.JazzerInternal"; + private static final String JAZZER_API_EXCEPTION = + "com.code_intelligence.jazzer.api.JazzerApiException"; private static final AtomicReference fuzzTestMethod = new AtomicReference<>(); private static Field lastFindingField; private static Field hooksEnabledField; @@ -114,6 +116,10 @@ private static void runWithHooks(Invocation invocation) throws Throwable { } catch (Throwable t) { thrown = t; } + // JazzerApiException signals API error, so propagate as is and not as a finding. + if (thrown != null && thrown.getClass().getName().equals(JAZZER_API_EXCEPTION)) { + throw thrown; + } Throwable stored = (Throwable) getLastFindingField().get(null); if (stored != null) { throw new FuzzTestFindingException(stored); From 05768ef57a3817ab0713287485177c5022864d65 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Fri, 20 Feb 2026 08:27:25 +0100 Subject: [PATCH 4/4] feat: add maximize() hill-climbing API to Jazzer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Jazzer.maximize(value, minValue, maxValue) for guiding the fuzzer to maximize a value over time. The user's value range is linearly mapped onto a fixed number of coverage counters (default 1024), avoiding the risk of exhausting counter space with large ranges. The effective counter count is capped at the actual range size when the range is smaller than the requested number. Three overloads are provided: - maximize(value, minValue, maxValue) — convenience, auto-generated id - maximize(value, minValue, maxValue, numCounters) — custom counter count - maximize(value, minValue, maxValue, numCounters, id) — full control Also wraps all Jazzer API methods with JazzerApiException error handling and adds a ReactorFuzzTest example demonstrating the maximize API. --- .../src/test/java/com/example/BUILD.bazel | 21 +++ .../java/com/example/ReactorFuzzTest.java | 59 +++++++ .../code_intelligence/jazzer/api/Jazzer.java | 164 ++++++++++++++++- .../jazzer/runtime/JazzerApiHooks.java | 35 ++++ .../code_intelligence/jazzer/api/BUILD.bazel | 21 +++ .../jazzer/api/MaximizeTest.java | 165 ++++++++++++++++++ 6 files changed, 459 insertions(+), 6 deletions(-) create mode 100644 examples/junit/src/test/java/com/example/ReactorFuzzTest.java create mode 100644 src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java 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..952b3b46a --- /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, 0, 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..353f2d598 100644 --- a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java +++ b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java @@ -21,6 +21,7 @@ import java.lang.invoke.MethodType; import java.lang.reflect.InvocationTargetException; import java.security.SecureRandom; +import java.util.concurrent.ConcurrentHashMap; /** Static helper methods that hooks can use to provide feedback to the fuzzer. */ public final class Jazzer { @@ -33,6 +34,19 @@ 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; + + /** + * Default number of counters allocated for each call site of a method that requires registering a + * range of artificial coverage counters, e.g., Jazzer maximize API. The user's value range is + * linearly mapped onto this many counters. + */ + public static final int DEFAULT_NUM_COUNTERS = 1024; + + /** Tracks the registered minValue and maxValue per maximize call-site id. */ + private static final ConcurrentHashMap idToRange = new ConcurrentHashMap<>(); + static { Class jazzerInternal = null; MethodHandle onFuzzTargetReady = null; @@ -40,6 +54,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 +86,16 @@ public final class Jazzer { tracePcIndir = MethodHandles.publicLookup() .findStatic(traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType); + + Class countersTracker = + Class.forName("com.code_intelligence.jazzer.runtime.ExtraCountersTracker"); + 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 +112,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 +121,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 @@ -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,120 @@ public static void exploreState(byte state) { // an automatically generated call-site id. Without instrumentation, this is a no-op. } + /** + * Core implementation of the hill-climbing maximize API. It maps {@code value} from the range + * [{@code minValue}, {@code maxValue}] onto {@code numCounters} coverage counters via linear + * interpolation, then sets all counters from 0 to the mapped offset. + * + *

Values below {@code minValue} produce no signal. Values above {@code maxValue} are clamped. + * + *

Must be invoked with the same {@code minValue}, {@code maxValue}, and {@code numCounters} + * for a given {@code id} across all calls. Passing different values is illegal. + * + * @param value the value to maximize + * @param minValue the minimum expected value (inclusive) + * @param maxValue the maximum expected value (inclusive); must be >= {@code minValue} + * @param numCounters the number of counters to allocate; must be > 0 + * @param id a unique identifier for this call site (must be consistent across runs) + * @throws JazzerApiException if {@code maxValue < minValue} or {@code numCounters <= 0} + */ + public static void maximize(long value, long minValue, long maxValue, int numCounters, int id) { + if (COUNTERS_TRACKER_ALLOCATE == null) { + return; + } + + try { + ensureRangeConsistent(id, minValue, maxValue); + int effectiveCounters = effectiveCounters(minValue, maxValue, numCounters); + COUNTERS_TRACKER_ALLOCATE.invokeExact(id, effectiveCounters); + + if (value >= minValue) { + int toOffset; + if (minValue == maxValue) { + toOffset = 0; + } else { + double range = (double) maxValue - (double) minValue; + double offset = (double) Math.min(value, maxValue) - (double) minValue; + toOffset = (int) (offset / range * (effectiveCounters - 1)); + } + COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset); + } + } catch (JazzerApiException e) { + throw e; + } catch (Throwable e) { + throw new JazzerApiException("maximize: " + e.getMessage(), e); + } + } + + private static void ensureRangeConsistent(int id, long minValue, long maxValue) { + long[] existing = idToRange.putIfAbsent(id, new long[] {minValue, maxValue}); + if (existing != null && (existing[0] != minValue || existing[1] != maxValue)) { + throw new IllegalArgumentException( + String.format( + "Range for id %d must remain constant across calls. " + + "Expected [%d, %d], but got [%d, %d].", + id, existing[0], existing[1], minValue, maxValue)); + } + } + + private static int effectiveCounters(long minValue, long maxValue, int maxNumCounters) { + if (maxValue < minValue) { + throw new IllegalArgumentException( + "maxValue (" + maxValue + ") must not be less than minValue (" + minValue + ")"); + } + if (maxNumCounters <= 0) { + throw new IllegalArgumentException( + "maxNumCounters (" + maxNumCounters + ") must be positive"); + } + + // Cap maxNumCounters at the actual range size to avoid wasting counters when the + // range is smaller than the requested number (e.g. range [0, 10] only needs 11). + double rangeSize = (double) maxValue - (double) minValue + 1; + return (rangeSize < maxNumCounters) ? (int) rangeSize : maxNumCounters; + } + + /** + * Convenience overload of {@link #maximize(long, long, long, int, int)} that uses {@link + * #DEFAULT_NUM_COUNTERS} counters and an automatically generated call-site id. + * + *

During instrumentation, calls to this method are replaced by a hook that supplies a unique + * id for each call site. Without instrumentation, this is a no-op. + * + *

{@code
+   * // Maximize temperature in [0, 4500]
+   * Jazzer.maximize(temperature, 0, 4500);
+   * }
+ * + * @param value the value to maximize + * @param minValue the minimum expected value (inclusive) + * @param maxValue the maximum expected value (inclusive) + * @see #maximize(long, long, long, int, int) + */ + public static void maximize(long value, long minValue, long maxValue) { + // Instrumentation replaces calls to this method with the core overload using + // DEFAULT_NUM_COUNTERS and an automatically generated call-site id. + // Without instrumentation, this is a no-op. + } + + /** + * Convenience overload of {@link #maximize(long, long, long, int, int)} that uses a custom number + * of counters and an automatically generated call-site id. + * + *

During instrumentation, calls to this method are replaced by a hook that supplies a unique + * id for each call site. Without instrumentation, this is a no-op. + * + * @param value the value to maximize + * @param minValue the minimum expected value (inclusive) + * @param maxValue the maximum expected value (inclusive) + * @param numCounters the number of counters to allocate; must be > 0 + * @see #maximize(long, long, long, int, int) + */ + public static void maximize(long value, long minValue, long maxValue, int numCounters) { + // Instrumentation replaces calls to this method with the core overload using + // the given numCounters and an automatically generated call-site id. + // Without instrumentation, this is a no-op. + } + /** * Make Jazzer report the provided {@link Throwable} as a finding. * @@ -261,8 +411,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/runtime/JazzerApiHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java index d1d9c1c01..ddd740e8e 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,39 @@ 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, long, long, int, int)} using {@link Jazzer#DEFAULT_NUM_COUNTERS} and the + * hook id. + */ + @MethodHook( + type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.api.Jazzer", + targetMethod = "maximize", + targetMethodDescriptor = "(JJJ)V") + public static void maximizeWithDefaultCountersAndId( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + Jazzer.maximize( + (long) arguments[0], + (long) arguments[1], + (long) arguments[2], + Jazzer.DEFAULT_NUM_COUNTERS, + hookId); + } + + /** + * Replaces calls to {@link Jazzer#maximize(long, long, long, int)} with calls to {@link + * Jazzer#maximize(long, long, long, int, int)} using the hook id. + */ + @MethodHook( + type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.api.Jazzer", + targetMethod = "maximize", + targetMethodDescriptor = "(JJJI)V") + public static void maximizeWithCustomCountersAndId( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + Jazzer.maximize( + (long) arguments[0], (long) arguments[1], (long) arguments[2], (int) arguments[3], hookId); + } } diff --git a/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel index 86014c7bd..b8f19e25a 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 = [ + "//src/main/java/com/code_intelligence/jazzer/runtime", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + "@maven//:junit_junit", + ], +) diff --git a/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java b/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java new file mode 100644 index 000000000..c0271ec37 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2026 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +public class MaximizeTest { + + @Test + public void testBasicRangeMapping() { + // value=50 in [0, 100] with 1024 counters → offset = 50/100 * 1023 = 511 + Jazzer.maximize(50, 0, 100, 1024, 500000); + } + + @Test + public void testValueAtMinimum() { + // value == minValue → offset = 0 + Jazzer.maximize(0, 0, 100, 1024, 500001); + } + + @Test + public void testValueAtMaximum() { + // value == maxValue → offset = numCounters - 1 + Jazzer.maximize(100, 0, 100, 1024, 500002); + } + + @Test + public void testValueBelowMinimum() { + // value < minValue → no signal (should not throw) + Jazzer.maximize(-10, 0, 100, 1024, 500003); + } + + @Test + public void testValueAboveMaximum() { + // value > maxValue → clamped to maxValue (offset = numCounters - 1) + Jazzer.maximize(200, 0, 100, 1024, 500004); + } + + @Test + public void testNegativeRange() { + // Range with negative values: [-100, -50] + Jazzer.maximize(-75, -100, -50, 1024, 500005); + } + + @Test + public void testSingleValueRange() { + // minValue == maxValue → offset is always 0 + Jazzer.maximize(42, 42, 42, 1024, 500006); + } + + @Test + public void testLargeRange() { + // Long.MIN_VALUE to Long.MAX_VALUE — should not overflow + Jazzer.maximize(0, Long.MIN_VALUE, Long.MAX_VALUE, 1024, 500007); + } + + @Test + public void testCustomNumCounters() { + // Expert overload with small counter count + Jazzer.maximize(50, 0, 100, 10, 500008); + } + + @Test + public void testZeroNumCountersThrows() { + try { + Jazzer.maximize(50, 0, 100, 0, 500009); + fail("Expected JazzerApiException for zero numCounters"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + } + + @Test + public void testNegativeNumCountersThrows() { + try { + Jazzer.maximize(50, 0, 100, -5, 500010); + fail("Expected JazzerApiException for negative numCounters"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must be positive")); + } + } + + @Test + public void testMaxValueLessThanMinValueThrows() { + try { + Jazzer.maximize(50, 100, 0, 1024, 500011); + fail("Expected JazzerApiException for maxValue < minValue"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must not be less than")); + } + } + + @Test + public void testMultipleCallsSameId() { + // Multiple calls with the same id should succeed (idempotent allocation) + Jazzer.maximize(10, 0, 100, 1024, 500012); + Jazzer.maximize(50, 0, 100, 1024, 500012); + Jazzer.maximize(90, 0, 100, 1024, 500012); + } + + @Test + public void testDifferentIdsWithDifferentRanges() { + Jazzer.maximize(50, 0, 100, 1024, 500013); + Jazzer.maximize(500, 0, 1000, 512, 500014); + } + + @Test + public void testInconsistentNumCountersThrows() { + // Same id and range but different numCounters (where range doesn't cap) + Jazzer.maximize(50, 0, 10000, 1024, 500017); + try { + Jazzer.maximize(50, 0, 10000, 2048, 500017); + fail("Expected JazzerApiException for inconsistent numCounters"); + } catch (JazzerApiException e) { + assertTrue( + e.getMessage().contains("numCounters") + && e.getMessage().contains("must remain constant")); + } + } + + @Test + public void testInconsistentMinValueThrows() { + Jazzer.maximize(50, 0, 100, 1024, 500015); + try { + Jazzer.maximize(50, 10, 100, 1024, 500015); + fail("Expected JazzerApiException for inconsistent minValue"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must remain constant")); + } + } + + @Test + public void testInconsistentMaxValueThrows() { + Jazzer.maximize(50, 0, 100, 1024, 500016); + try { + Jazzer.maximize(50, 0, 200, 1024, 500016); + fail("Expected JazzerApiException for inconsistent maxValue"); + } catch (JazzerApiException e) { + assertTrue(e.getMessage().contains("must remain constant")); + } + } + + @Test + public void testDefaultNumCountersConstant() { + assertEquals(1024, Jazzer.DEFAULT_NUM_COUNTERS); + } +}