Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ java_test(

java_library(
name = "coverage_instrumentation_benchmark",
srcs = ["CoverageInstrumentationBenchmark.java"],
srcs = ["CoverageInstrumentationBenchmark.java"] + select({
":jdk25": ["FfmCoverageInstrumentationBenchmark.java"],
"//conditions:default": [],
}),
plugins = ["//src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"],
runtime_deps = [
"@maven//:com_mikesamuel_json_sanitizer",
Expand All @@ -39,14 +42,20 @@ java_library(
":strategies",
"//src/main/java/com/code_intelligence/jazzer/instrumentor",
"@maven//:org_openjdk_jmh_jmh_core",
],
] + select({
":jdk25": [":ffm_strategies"],
"//conditions:default": [],
}),
)

java_library(
name = "strategies",
srcs = [
"DirectByteBuffer2CoverageMap.java",
"DirectByteBufferCoverageMap.java",
# Uses the FFM API only via method handles, so it compiles with Java 8 source level, but
# only works on JDK 22+.
"FfmMethodHandleCoverageMap.java",
"Unsafe2CoverageMap.java",
"UnsafeBranchfreeCoverageMap.java",
"UnsafeCoverageMap.java",
Expand All @@ -59,6 +68,30 @@ java_library(
],
)

# The FFM API requires JDK 22+, so the FFM-backed coverage maps are only benchmarked when building
# with remotejdk_25, the only remote JDK >= 22 provided by rules_java (it ships LTS releases only):
# bazel run --java_runtime_version=remotejdk_25 \
# //src/jmh/java/com/code_intelligence/jazzer/instrumentor:CoverageInstrumentationBenchmark
config_setting(
name = "jdk25",
values = {"java_runtime_version": "remotejdk_25"},
)

java_library(
name = "ffm_strategies",
srcs = ["FfmCoverageMap.java"],
javacopts = [
"-source",
"22",
"-target",
"22",
],
target_compatible_with = select({
":jdk25": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
)

kt_jvm_library(
name = "kotlin_strategies",
srcs = ["DirectByteBufferStrategy.kt"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ public class CoverageInstrumentationBenchmark {
private static final String TARGET_METHOD = "sanitize";
private static final MethodType TARGET_TYPE = MethodType.methodType(String.class, String.class);

// This is part of the benchmark's state and not a constant to prevent constant folding.
String TARGET_ARG =
static final String SAMPLE_INPUT =
"{\"foo\":1123987,\"bar\":[true, false],\"baz\":{\"foo\":\"132ä3\",\"bar\":1.123e-005}}";

// This is part of the benchmark's state and not a constant to prevent constant folding.
String TARGET_ARG = SAMPLE_INPUT;

MethodHandle uninstrumented_sanitize;
MethodHandle local_DirectByteBuffer_NeverZero_sanitize;
MethodHandle staticMethod_DirectByteBuffer_NeverZero_sanitize;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.instrumentor;

import java.lang.invoke.MethodHandle;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;

/**
* Complements {@link CoverageInstrumentationBenchmark} with coverage maps backed by the Foreign
* Function & Memory API. Since that API requires JDK 22+, this class is only compiled when building
* with {@code --java_runtime_version=remotejdk_25}, see BUILD.bazel.
*/
@State(Scope.Benchmark)
public class FfmCoverageInstrumentationBenchmark {
// This is part of the benchmark's state and not a constant to prevent constant folding.
String TARGET_ARG = CoverageInstrumentationBenchmark.SAMPLE_INPUT;

MethodHandle staticMethod_Ffm_NeverZero_sanitize;
MethodHandle staticMethod_FfmMethodHandle_NeverZero_sanitize;

@Setup
public void instrumentWithStrategies()
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
staticMethod_Ffm_NeverZero_sanitize =
CoverageInstrumentationBenchmark.instrumentWithStrategy(
new StaticMethodStrategy(), FfmCoverageMap.class);
staticMethod_FfmMethodHandle_NeverZero_sanitize =
CoverageInstrumentationBenchmark.instrumentWithStrategy(
new StaticMethodStrategy(), FfmMethodHandleCoverageMap.class);
}

@Benchmark
public String staticMethod_Ffm_NeverZero() throws Throwable {
return (String) staticMethod_Ffm_NeverZero_sanitize.invokeExact(TARGET_ARG);
}

@Benchmark
public String staticMethod_FfmMethodHandle_NeverZero() throws Throwable {
return (String) staticMethod_FfmMethodHandle_NeverZero_sanitize.invokeExact(TARGET_ARG);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.instrumentor;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;

/** A coverage map backed by the Foreign Function & Memory API (JDK 22+). */
public final class FfmCoverageMap {
// The current target, JsonSanitizer, uses less than 2048 coverage counters.
private static final long NUM_COUNTERS = 4096;

// Zero-initialized native memory that is never freed. Its address() could be handed to libFuzzer
// just like the address of the Unsafe-backed map.
private static final MemorySegment counters = Arena.global().allocate(NUM_COUNTERS);

public static void enlargeIfNeeded(int nextId) {
// Statically sized counters buffer.
}

public static void recordCoverage(final int id) {
final byte counter = counters.get(ValueLayout.JAVA_BYTE, id);
counters.set(ValueLayout.JAVA_BYTE, id, (byte) (counter == -1 ? 1 : counter + 1));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.instrumentor;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
* Same as {@link FfmCoverageMap}, but only accesses the FFM API (JDK 22+) via method handles. This
* class therefore compiles with Java 8 source level and could ship in the same jar as the
* Unsafe-backed coverage map, as long as it is only loaded on JDK 22+.
*/
public final class FfmMethodHandleCoverageMap {
// The current target, JsonSanitizer, uses less than 2048 coverage counters.
private static final long NUM_COUNTERS = 4096;

// MemorySegment#get(ValueLayout.OfByte, long) with the segment and layout bound: (long)byte
private static final MethodHandle GET_BYTE;
// MemorySegment#set(ValueLayout.OfByte, long, byte) with the segment and layout bound:
// (long,byte)void
private static final MethodHandle SET_BYTE;

static {
try {
Class<?> arena = Class.forName("java.lang.foreign.Arena");
Class<?> memorySegment = Class.forName("java.lang.foreign.MemorySegment");
Class<?> ofByte = Class.forName("java.lang.foreign.ValueLayout$OfByte");
MethodHandles.Lookup lookup = MethodHandles.publicLookup();

Object javaByte =
Class.forName("java.lang.foreign.ValueLayout").getField("JAVA_BYTE").get(null);
Object globalArena =
lookup.findStatic(arena, "global", MethodType.methodType(arena)).invoke();
// Zero-initialized native memory that is never freed. Its address() could be handed to
// libFuzzer just like the address of the Unsafe-backed map.
Object counters =
lookup
.findVirtual(arena, "allocate", MethodType.methodType(memorySegment, long.class))
.invoke(globalArena, NUM_COUNTERS);

GET_BYTE =
MethodHandles.insertArguments(
lookup.findVirtual(
memorySegment, "get", MethodType.methodType(byte.class, ofByte, long.class)),
0,
counters,
javaByte);
SET_BYTE =
MethodHandles.insertArguments(
lookup.findVirtual(
memorySegment,
"set",
MethodType.methodType(void.class, ofByte, long.class, byte.class)),
0,
counters,
javaByte);
} catch (Throwable t) {
throw new ExceptionInInitializerError(t);
}
}

public static void enlargeIfNeeded(int nextId) {
// Statically sized counters buffer.
}

public static void recordCoverage(final int id) {
try {
final byte counter = (byte) GET_BYTE.invokeExact((long) id);
SET_BYTE.invokeExact((long) id, (byte) (counter == -1 ? 1 : counter + 1));
} catch (Throwable t) {
throw new IllegalStateException("Failed to record coverage", t);
}
}
}
Loading