Skip to content
Merged
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
20 changes: 19 additions & 1 deletion .github/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,28 @@ xcode_version(
version = "15.2.0.15C500b",
)

# Xcode version on public GitHub Actions macos-15 runners
xcode_version(
name = "version16_4_0_16F6",
aliases = [
"16.4.0.16F6",
"16F6",
"16.4.0",
"16.4",
"16",
],
default_ios_sdk_version = "18.5",
default_macos_sdk_version = "15.5",
default_tvos_sdk_version = "18.5",
default_watchos_sdk_version = "11.5",
version = "16.4.0.16F6",
)

xcode_config(
name = "host_xcodes",
default = ":version15_2_0_15C500b",
default = ":version16_4_0_16F6",
versions = [
":version16_4_0_16F6",
":version15_2_0_15C500b",
":version14_2_0_14C18",
],
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/fuzzing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-14]
os: [ubuntu-22.04, macos-15]
jdk: [21, 8]
include:
- jdk: 21
# Workaround for https://github.com/bazelbuild/bazel/issues/14502
extra_bazel_args: "--jvmopt=-Djava.security.manager=allow"
- os: ubuntu-22.04
arch: "linux"
- os: macos-14
- os: macos-15
arch: "macos-arm64"
bazel_args: "--xcode_version_config=//.github:host_xcodes"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prerelease.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
name: linux-x86-64
- os: ubuntu-22.04-arm
name: linux-arm64
- os: macos-14
- os: macos-15
name: macos
- os: windows-2022
name: windows
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/run-all-tests-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
name: Build & Test
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-22.04-arm, windows-2022, macos-14]
os: [ubuntu-22.04, ubuntu-22.04-arm, windows-2022, macos-15]
jdk: [21, 8]
include:
- jdk: 21
Expand All @@ -28,7 +28,7 @@ jobs:
arch: "linux"
- os: ubuntu-22.04-arm
arch: "linux-arm64"
- os: macos-14
- os: macos-15
arch: "macos-arm64"
bazel_args: "--xcode_version_config=//.github:host_xcodes"
- os: windows-2022
Expand Down
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);
}
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/code_intelligence/jazzer/Jazzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ private static void start(List<String> args) throws IOException, InterruptedExce
Set<String> argsToFilter =
Stream.of("--asan", "--ubsan", "--hwasan", "--native").collect(toSet());
ProcessBuilder processBuilder = new ProcessBuilder();
// The JVM handles these signals itself, e.g. for implicit null checks and safepoint polls. A
// UBSan runtime that is only loaded once the JVM is running, such as the one clang on macOS
// links into every library built with -fsanitize=fuzzer-no-link, would otherwise replace the
// JVM's handlers and turn these signals into crashes.
processBuilder
.environment()
.compute(
"UBSAN_OPTIONS",
(name, currentValue) ->
appendWithPathListSeparator(
name, "handle_segv=0", "handle_sigbus=0", "handle_sigfpe=0"));
List<Path> preloadLibs = new ArrayList<>();
// We have to load jazzer_preload before we load ASan since the ASan includes no-op definitions
// of the fuzzer callbacks as weak symbols, but the dynamic linker doesn't distinguish between
Expand Down
Loading
Loading