From be22c816825d2ba2730f43bddc0782d63fbb4b94 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Wed, 14 Oct 2026 12:57:52 +0200 Subject: [PATCH 1/4] chore: add FFM coverage map benchmarks sun.misc.Unsafe memory access is deprecated for removal (JEP 471/498) and warns on JDK 24+. Add coverage maps backed by the Foreign Function & Memory API to CoverageInstrumentationBenchmark to compare them with the Unsafe-backed map Jazzer uses today: - FfmCoverageMap uses MemorySegment directly (Java 22 source). - FfmMethodHandleCoverageMap accesses the same API via method handles, so it compiles with Java 8 source level and could ship in the same jar as the Unsafe-backed map. Since FFM requires JDK 22+, the FFM benchmarks are only built with --java_runtime_version=remotejdk_25. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc8e7077-31cc-47a3-acb9-050440f42ea5 --- .../jazzer/instrumentor/BUILD.bazel | 37 +++++++- .../CoverageInstrumentationBenchmark.java | 6 +- .../FfmCoverageInstrumentationBenchmark.java | 58 ++++++++++++ .../jazzer/instrumentor/FfmCoverageMap.java | 40 +++++++++ .../FfmMethodHandleCoverageMap.java | 89 +++++++++++++++++++ 5 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageInstrumentationBenchmark.java create mode 100644 src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmCoverageMap.java create mode 100644 src/jmh/java/com/code_intelligence/jazzer/instrumentor/FfmMethodHandleCoverageMap.java 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); + } + } +} From f955083a69c54a6489cbfd00500c79fd666cc434 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 9 Nov 2026 09:34:37 +0100 Subject: [PATCH 2/4] ci: update macOS runners to macos-15 GitHub retired the macos-14 runner image on 2026-11-02. Use Xcode 16.4, the default on macos-15. --- .github/BUILD.bazel | 20 +++++++++++++++++++- .github/workflows/fuzzing.yml | 4 ++-- .github/workflows/prerelease.yaml | 2 +- .github/workflows/run-all-tests-pr.yml | 4 ++-- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/BUILD.bazel b/.github/BUILD.bazel index 8b0c07fc1..63d90ac34 100644 --- a/.github/BUILD.bazel +++ b/.github/BUILD.bazel @@ -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", ], diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 4bfca1cf4..d36a8a631 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, macos-14] + os: [ubuntu-22.04, macos-15] jdk: [21, 8] include: - jdk: 21 @@ -28,7 +28,7 @@ jobs: 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" diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index f621c0db9..29004c065 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -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 diff --git a/.github/workflows/run-all-tests-pr.yml b/.github/workflows/run-all-tests-pr.yml index 6720bf5c7..8f8663d09 100644 --- a/.github/workflows/run-all-tests-pr.yml +++ b/.github/workflows/run-all-tests-pr.yml @@ -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 @@ -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 From 0dd8544dcc2723e7a14516fcaed7f135076f34e9 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 9 Nov 2026 11:17:57 +0100 Subject: [PATCH 3/4] fix: make coverage IDs of retransformed classes deterministic The JVM reconstitutes retransformed classes with their methods in an order that varies between runs. Since coverage and hook IDs are assigned in method order, fuzzing with a fixed seed wasn't reproducible, e.g. ReactorFuzzTest timed out on macOS CI with JDK 8. Sort the methods first. --- .../jazzer/agent/RuntimeInstrumentor.kt | 10 +++- .../jazzer/instrumentor/ClassInstrumentor.kt | 20 +++++++ .../jazzer/instrumentor/BUILD.bazel | 20 +++++++ .../jazzer/instrumentor/SortMethodsTest.kt | 53 +++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/code_intelligence/jazzer/instrumentor/SortMethodsTest.kt diff --git a/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt index 3a44ed1e5..734ab91b0 100644 --- a/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt @@ -20,6 +20,7 @@ import com.code_intelligence.jazzer.instrumentor.ClassInstrumentor import com.code_intelligence.jazzer.instrumentor.CoverageRecorder import com.code_intelligence.jazzer.instrumentor.Hook import com.code_intelligence.jazzer.instrumentor.InstrumentationType +import com.code_intelligence.jazzer.instrumentor.sortMethods import com.code_intelligence.jazzer.utils.ClassNameGlobber import com.code_intelligence.jazzer.utils.Log import io.github.classgraph.ClassGraph @@ -105,7 +106,11 @@ class RuntimeInstrumentor( // Using ClassWriter.COMPUTE_FRAMES as an escape hatch isn't possible in the context of an agent as the // computation may itself need to load classes, which leads to circular loads and incompatible class // redefinitions. - transformInternal(internalClassName, classfileBuffer.takeUnless { loader == null && classBeingRedefined != null }) + transformInternal( + internalClassName, + classfileBuffer.takeUnless { loader == null && classBeingRedefined != null }, + isReconstituted = classBeingRedefined != null, + ) } catch (t: Throwable) { if (dumpClassesDir != null) { dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".failed", pathPrefix = pathPrefix) @@ -180,6 +185,7 @@ class RuntimeInstrumentor( fun transformInternal( internalClassName: String, maybeClassfileBuffer: ByteArray?, + isReconstituted: Boolean = false, ): ByteArray? { val (fullInstrumentation, printInfo) = when { @@ -193,7 +199,7 @@ class RuntimeInstrumentor( } val className = internalClassName.replace('/', '.') val classfileBuffer = - maybeClassfileBuffer ?: ClassGraph() + maybeClassfileBuffer?.let { if (isReconstituted) sortMethods(it) else it } ?: ClassGraph() .enableSystemJarsAndModules() .acceptLibOrExtJars() .ignoreClassVisibility() diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt index 51617ab0f..7e341dee9 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt @@ -17,10 +17,30 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.runtime.CoverageMap +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.tree.ClassNode fun extractClassFileMajorVersion(classfileBuffer: ByteArray): Int = ((classfileBuffer[6].toInt() and 0xff) shl 8) or (classfileBuffer[7].toInt() and 0xff) +/** + * Sorts the methods of the given class file by name and descriptor. + * + * Coverage and hook IDs are assigned in method order. The class file the JVM reconstitutes for a + * retransformation lists methods in an internal order that can differ between JVM runs, which would make the + * IDs, and thus fuzzing with a fixed seed, nondeterministic. + */ +fun sortMethods(classfileBuffer: ByteArray): ByteArray { + val reader = ClassReader(classfileBuffer) + val node = ClassNode() + reader.accept(node, 0) + node.methods.sortWith(compareBy({ it.name }, { it.desc })) + val writer = ClassWriter(reader, 0) + node.accept(writer) + return writer.toByteArray() +} + class ClassInstrumentor( private val internalClassName: String, bytecode: ByteArray, diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 7d0cf08ac..126e10d1c 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -66,6 +66,26 @@ wrapped_kt_jvm_test( ], ) +wrapped_kt_jvm_test( + name = "sort_methods_test", + size = "small", + srcs = [ + "CoverageInstrumentationTarget.java", + "SortMethodsTest.kt", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.SortMethodsTest", + deps = [ + ":patch_test_utils", + "@maven//:junit_junit", + "@maven//:org_ow2_asm_asm", + "@maven//:org_ow2_asm_asm_tree", + "@rules_kotlin//kotlin/compiler:kotlin-test", + ], +) + wrapped_kt_jvm_test( name = "hook_validation_test", size = "small", diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/SortMethodsTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/SortMethodsTest.kt new file mode 100644 index 000000000..d6ac0560b --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/SortMethodsTest.kt @@ -0,0 +1,53 @@ +/* + * 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 com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode +import org.junit.Test +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.tree.ClassNode +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class SortMethodsTest { + private fun readClass(bytecode: ByteArray): ClassNode = ClassNode().also { ClassReader(bytecode).accept(it, 0) } + + private fun methodSignatures(bytecode: ByteArray): List> = readClass(bytecode).methods.map { it.name to it.desc } + + private fun reverseMethods(bytecode: ByteArray): ByteArray { + val reader = ClassReader(bytecode) + val node = ClassNode() + reader.accept(node, 0) + node.methods.reverse() + val writer = ClassWriter(reader, 0) + node.accept(writer) + return writer.toByteArray() + } + + @Test + fun testSortMethodsIsIndependentOfMethodOrder() { + val original = classToBytecode(CoverageInstrumentationTarget::class.java) + val reversed = reverseMethods(original) + assertNotEquals(methodSignatures(original), methodSignatures(reversed)) + + val sorted = sortMethods(original) + assertEquals(methodSignatures(original).sortedWith(compareBy({ it.first }, { it.second })), methodSignatures(sorted)) + assertContentEquals(sorted, sortMethods(reversed)) + } +} From bdebf95c4de0154f7e6f1666a64d2b951b4a6861 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 9 Nov 2026 11:17:57 +0100 Subject: [PATCH 4/4] fix: keep UBSan from replacing the JVM's signal handlers On macOS, libraries built with -fsanitize=fuzzer-no-link load the UBSan runtime, which then turns the JVM's own SIGSEGV/SIGBUS into crashes, e.g. in NativeValueProfileFuzzer. --- .../java/com/code_intelligence/jazzer/Jazzer.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/code_intelligence/jazzer/Jazzer.java b/src/main/java/com/code_intelligence/jazzer/Jazzer.java index 999092a35..1a7b059c4 100644 --- a/src/main/java/com/code_intelligence/jazzer/Jazzer.java +++ b/src/main/java/com/code_intelligence/jazzer/Jazzer.java @@ -128,6 +128,17 @@ private static void start(List args) throws IOException, InterruptedExce Set 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 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