diff --git a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 0e90fb0e2..a2c7e746a 100644 --- a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -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", @@ -39,7 +42,10 @@ 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( @@ -47,6 +53,9 @@ java_library( 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", @@ -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"], diff --git a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java index ac2d797a3..f23480f0e 100644 --- a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java +++ b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java @@ -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; diff --git a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageInstrumentationBenchmark.java b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageInstrumentationBenchmark.java new file mode 100644 index 000000000..b68577396 --- /dev/null +++ b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageInstrumentationBenchmark.java @@ -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); + } +} diff --git a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageMap.java b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageMap.java new file mode 100644 index 000000000..996c26210 --- /dev/null +++ b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageMap.java @@ -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)); + } +} diff --git a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmMethodHandleCoverageMap.java b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmMethodHandleCoverageMap.java new file mode 100644 index 000000000..dfa8c1784 --- /dev/null +++ b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmMethodHandleCoverageMap.java @@ -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); + } + } +}