From d40b6e8a55d2823affcd6f8ef5bb950420c65c1c Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Fri, 28 Jan 2022 14:49:51 +0100 Subject: Benchmark EdgeCoverageStrategies in isolation --- .../jazzer/instrumentor/BUILD.bazel | 29 +++++ .../CoverageInstrumentationBenchmark.java | 130 +++++++++++++++++++++ .../instrumentor/DirectByteBufferCoverageMap.java | 27 +++++ 3 files changed, 186 insertions(+) create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel new file mode 100644 index 00000000..d2c9d965 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -0,0 +1,29 @@ +java_binary( + name = "CoverageInstrumentationBenchmark", + srcs = [ + "CoverageInstrumentationBenchmark.java", + ], + main_class = "org.openjdk.jmh.Main", + plugins = [":JmhGeneratorAnnotationProcessor"], + runtime_deps = [ + "@maven//:com_mikesamuel_json_sanitizer", + ], + deps = [ + ":strategies", + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) + +java_library( + name = "strategies", + srcs = [ + "DirectByteBufferCoverageMap.java", + ], +) + +java_plugin( + name = "JmhGeneratorAnnotationProcessor", + processor_class = "org.openjdk.jmh.generators.BenchmarkProcessor", + deps = ["@maven//:org_openjdk_jmh_jmh_generator_annprocess"], +) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java new file mode 100644 index 00000000..f8609982 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java @@ -0,0 +1,130 @@ +// Copyright 2022 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +/** + * This benchmark compares the throughput of a typical fuzz target when instrumented with different + * edge coverage instrumentation strategies and coverage map implementations. + * + * The benchmark currently uses the OWASP json-sanitizer as its target, which has the following + * desirable properties for a benchmark: + * - It is a reasonably sized project that does not consist of many different classes. + * - It is very heavy on computation with a high density of branching. + * - It is entirely CPU bound with no IO and does not call expensive methods from the standard + * library. + * With these properties, results obtained from this benchmark should provide reasonable lower + * bounds on the relative slowdown introduced by the various approaches to instrumentations. + */ +@State(Scope.Benchmark) +public class CoverageInstrumentationBenchmark { + private static final String TARGET_CLASSNAME = "com.google.json.JsonSanitizer"; + private static final String TARGET_PACKAGE = + TARGET_CLASSNAME.substring(0, TARGET_CLASSNAME.lastIndexOf('.')); + 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 = + "{\"foo\":1123987,\"bar\":[true, false],\"baz\":{\"foo\":\"132ä3\",\"bar\":1.123e-005}}"; + + MethodHandle uninstrumented_sanitize; + MethodHandle local_DirectByteBuffer_NeverZero_sanitize; + + public static MethodHandle handleForTargetMethod(ClassLoader classLoader) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + Class targetClass = classLoader.loadClass(TARGET_CLASSNAME); + return MethodHandles.lookup().findStatic(targetClass, TARGET_METHOD, TARGET_TYPE); + } + + public static MethodHandle instrumentWithStrategy( + EdgeCoverageStrategy strategy, Class coverageMapClass) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + if (strategy == null) { + // Do not instrument the code by using the benchmark class' ClassLoader. + return handleForTargetMethod(CoverageInstrumentationBenchmark.class.getClassLoader()); + } + // It's fine to reuse a single instrumentor here as we don't want to know which class received + // how many counters. + Instrumentor instrumentor = new EdgeCoverageInstrumentor(strategy, coverageMapClass, 0); + return handleForTargetMethod(new InstrumentingClassLoader(instrumentor, TARGET_PACKAGE)); + } + + @Setup + public void instrumentWithStrategies() + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + uninstrumented_sanitize = instrumentWithStrategy(null, null); + local_DirectByteBuffer_NeverZero_sanitize = instrumentWithStrategy( + DirectByteBufferStrategy.INSTANCE, DirectByteBufferCoverageMap.class); + } + + @Benchmark + public String uninstrumented() throws Throwable { + return (String) uninstrumented_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String local_DirectByteBuffer_NeverZero() throws Throwable { + return (String) local_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG); + } +} + +class InstrumentingClassLoader extends ClassLoader { + private final Instrumentor instrumentor; + private final String classNamePrefix; + + InstrumentingClassLoader(Instrumentor instrumentor, String packageToInstrument) { + super(InstrumentingClassLoader.class.getClassLoader()); + this.instrumentor = instrumentor; + this.classNamePrefix = packageToInstrument + "."; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (!name.startsWith(classNamePrefix)) { + return super.loadClass(name); + } + try (InputStream stream = super.getResourceAsStream(name.replace('.', '/') + ".class")) { + if (stream == null) { + throw new ClassNotFoundException(String.format("Failed to find class file for %s", name)); + } + byte[] bytecode = readAllBytes(stream); + byte[] instrumentedBytecode = instrumentor.instrument(bytecode); + return defineClass(name, instrumentedBytecode, 0, instrumentedBytecode.length); + } catch (IOException e) { + throw new ClassNotFoundException(String.format("Failed to read class file for %s", name), e); + } + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[64 * 104 * 1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java new file mode 100644 index 00000000..6a143991 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java @@ -0,0 +1,27 @@ +// Copyright 2022 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.nio.ByteBuffer; + +public final class DirectByteBufferCoverageMap { + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final int NUM_COUNTERS = 4096; + public static final ByteBuffer counters = ByteBuffer.allocateDirect(NUM_COUNTERS); + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } +} -- cgit v1.2.3 From cd2b0e49d3497eef381c843e1a548fe75e049531 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Fri, 28 Jan 2022 15:06:13 +0100 Subject: Benchmark a DirectByteBuffer strategy using a static method This strategy uses a static method call instead of inlined coverage counter update logic, which greatly simplifies the complexity of the injected bytecode. --- .../jazzer/instrumentor/BUILD.bazel | 6 +++ .../CoverageInstrumentationBenchmark.java | 8 ++++ .../instrumentor/DirectByteBufferCoverageMap.java | 9 ++++ .../jazzer/instrumentor/StaticMethodStrategy.java | 48 ++++++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index d2c9d965..8c19c01e 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -19,6 +19,12 @@ java_library( name = "strategies", srcs = [ "DirectByteBufferCoverageMap.java", + "StaticMethodStrategy.java", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@jazzer_jacoco//:jacoco_internal", + "@jazzer_ow2_asm//:asm", ], ) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java index f8609982..5b051b5c 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java @@ -52,6 +52,7 @@ public class CoverageInstrumentationBenchmark { MethodHandle uninstrumented_sanitize; MethodHandle local_DirectByteBuffer_NeverZero_sanitize; + MethodHandle staticMethod_DirectByteBuffer_NeverZero_sanitize; public static MethodHandle handleForTargetMethod(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { @@ -78,6 +79,8 @@ public class CoverageInstrumentationBenchmark { uninstrumented_sanitize = instrumentWithStrategy(null, null); local_DirectByteBuffer_NeverZero_sanitize = instrumentWithStrategy( DirectByteBufferStrategy.INSTANCE, DirectByteBufferCoverageMap.class); + staticMethod_DirectByteBuffer_NeverZero_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBufferCoverageMap.class); } @Benchmark @@ -89,6 +92,11 @@ public class CoverageInstrumentationBenchmark { public String local_DirectByteBuffer_NeverZero() throws Throwable { return (String) local_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG); } + + @Benchmark + public String staticMethod_DirectByteBuffer_NeverZero() throws Throwable { + return (String) staticMethod_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG); + } } class InstrumentingClassLoader extends ClassLoader { diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java index 6a143991..e5e66abb 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java @@ -24,4 +24,13 @@ public final class DirectByteBufferCoverageMap { public static void enlargeIfNeeded(int nextId) { // Statically sized counters buffer. } + + public static void recordCoverage(final int id) { + final byte counter = counters.get(id); + if (counter == -1) { + counters.put(id, (byte) 1); + } else { + counters.put(id, (byte) (counter + 1)); + } + } } diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java new file mode 100644 index 00000000..ca0bd3d7 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java @@ -0,0 +1,48 @@ +// Copyright 2022 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 org.jacoco.core.internal.instr.InstrSupport; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class StaticMethodStrategy implements EdgeCoverageStrategy { + @Override + public void instrumentControlFlowEdge( + MethodVisitor mv, int edgeId, int variable, String coverageMapInternalClassName) { + InstrSupport.push(mv, edgeId); + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, coverageMapInternalClassName, "recordCoverage", "(I)V", false); + } + + @Override + public int getInstrumentControlFlowEdgeStackSize() { + return 1; + } + + @Override + public Object getLocalVariableType() { + return null; + } + + @Override + public void loadLocalVariable( + MethodVisitor mv, int variable, String coverageMapInternalClassName) {} + + @Override + public int getLoadLocalVariableStackSize() { + return 0; + } +} -- cgit v1.2.3 From 66f355248d5be7875aa23d6d259b6689a5364a70 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Fri, 28 Jan 2022 15:16:28 +0100 Subject: Benchmark an Unsafe strategy using a static method This strategy uses a static method call instead of inlined coverage counter update logic, which greatly simplifies the complexity of the injected bytecode. Instead of a DirectByteBuffer, it uses Unsafe to circumvent bounds checks. --- .../jazzer/instrumentor/BUILD.bazel | 1 + .../CoverageInstrumentationBenchmark.java | 8 +++ .../jazzer/instrumentor/UnsafeCoverageMap.java | 59 ++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeCoverageMap.java (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 8c19c01e..bd9dfcc4 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -20,6 +20,7 @@ java_library( srcs = [ "DirectByteBufferCoverageMap.java", "StaticMethodStrategy.java", + "UnsafeCoverageMap.java", ], deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java index 5b051b5c..4424d303 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java @@ -53,6 +53,7 @@ public class CoverageInstrumentationBenchmark { MethodHandle uninstrumented_sanitize; MethodHandle local_DirectByteBuffer_NeverZero_sanitize; MethodHandle staticMethod_DirectByteBuffer_NeverZero_sanitize; + MethodHandle staticMethod_Unsafe_NeverZero_sanitize; public static MethodHandle handleForTargetMethod(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { @@ -81,6 +82,8 @@ public class CoverageInstrumentationBenchmark { DirectByteBufferStrategy.INSTANCE, DirectByteBufferCoverageMap.class); staticMethod_DirectByteBuffer_NeverZero_sanitize = instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBufferCoverageMap.class); + staticMethod_Unsafe_NeverZero_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), UnsafeCoverageMap.class); } @Benchmark @@ -97,6 +100,11 @@ public class CoverageInstrumentationBenchmark { public String staticMethod_DirectByteBuffer_NeverZero() throws Throwable { return (String) staticMethod_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG); } + + @Benchmark + public String staticMethod_Unsafe_NeverZero() throws Throwable { + return (String) staticMethod_Unsafe_NeverZero_sanitize.invokeExact(TARGET_ARG); + } } class InstrumentingClassLoader extends ClassLoader { diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeCoverageMap.java new file mode 100644 index 00000000..cf73928d --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeCoverageMap.java @@ -0,0 +1,59 @@ +// Copyright 2022 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.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeCoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + final byte counter = UNSAFE.getByte(address); + if (counter == -1) { + UNSAFE.putByte(address, (byte) 1); + } else { + UNSAFE.putByte(address, (byte) (counter + 1)); + } + } +} -- cgit v1.2.3 From c5dce3937772c9d703db557a5164df7aa565f6a5 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Sun, 30 Jan 2022 09:13:19 +0100 Subject: Run a light version of the benchmark as a test --- .../jazzer/instrumentor/BUILD.bazel | 32 ++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index bd9dfcc4..4e417ef5 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -1,9 +1,37 @@ java_binary( name = "CoverageInstrumentationBenchmark", - srcs = [ - "CoverageInstrumentationBenchmark.java", + main_class = "org.openjdk.jmh.Main", + runtime_deps = [ + ":coverage_instrumentation_benchmark", + ], +) + +java_test( + name = "CoverageInstrumentationBenchmarkTest", + args = [ + # Fail fast on any exceptions produced by benchmarks. + "-foe true", + "-wf 1", + "-f 1", + "-wi 1", + "-i 1", + "-r 1s", + "-w 1s", + ], + jvm_flags = [ + "-XX:CompileCommand=print,*CoverageMap.recordCoverage", ], main_class = "org.openjdk.jmh.Main", + # Directly invoke JMH's main without using a testrunner. + use_testrunner = False, + runtime_deps = [ + ":coverage_instrumentation_benchmark", + ], +) + +java_library( + name = "coverage_instrumentation_benchmark", + srcs = ["CoverageInstrumentationBenchmark.java"], plugins = [":JmhGeneratorAnnotationProcessor"], runtime_deps = [ "@maven//:com_mikesamuel_json_sanitizer", -- cgit v1.2.3 From 2735c60232d1c536cfd72ffc0d89a38647a66ba4 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 31 Jan 2022 23:15:08 +0100 Subject: Add more coverage map versions to the benchmark --- .../jazzer/instrumentor/BUILD.bazel | 4 ++ .../CoverageInstrumentationBenchmark.java | 32 +++++++++++++ .../instrumentor/DirectByteBuffer2CoverageMap.java | 32 +++++++++++++ .../jazzer/instrumentor/Unsafe2CoverageMap.java | 55 ++++++++++++++++++++++ .../instrumentor/UnsafeBranchfreeCoverageMap.java | 55 ++++++++++++++++++++++ .../UnsafeSimpleIncrementCoverageMap.java | 54 +++++++++++++++++++++ 6 files changed, 232 insertions(+) create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBuffer2CoverageMap.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/Unsafe2CoverageMap.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeBranchfreeCoverageMap.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeSimpleIncrementCoverageMap.java (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 4e417ef5..e8858f0f 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -46,9 +46,13 @@ java_library( java_library( name = "strategies", srcs = [ + "DirectByteBuffer2CoverageMap.java", "DirectByteBufferCoverageMap.java", "StaticMethodStrategy.java", + "Unsafe2CoverageMap.java", + "UnsafeBranchfreeCoverageMap.java", "UnsafeCoverageMap.java", + "UnsafeSimpleIncrementCoverageMap.java", ], deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java index 4424d303..f388c4cc 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java @@ -53,7 +53,11 @@ public class CoverageInstrumentationBenchmark { MethodHandle uninstrumented_sanitize; MethodHandle local_DirectByteBuffer_NeverZero_sanitize; MethodHandle staticMethod_DirectByteBuffer_NeverZero_sanitize; + MethodHandle staticMethod_DirectByteBuffer2_NeverZero_sanitize; MethodHandle staticMethod_Unsafe_NeverZero_sanitize; + MethodHandle staticMethod_Unsafe_NeverZero2_sanitize; + MethodHandle staticMethod_Unsafe_NeverZeroBranchfree_sanitize; + MethodHandle staticMethod_Unsafe_SimpleIncrement_sanitize; public static MethodHandle handleForTargetMethod(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { @@ -82,8 +86,16 @@ public class CoverageInstrumentationBenchmark { DirectByteBufferStrategy.INSTANCE, DirectByteBufferCoverageMap.class); staticMethod_DirectByteBuffer_NeverZero_sanitize = instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBufferCoverageMap.class); + staticMethod_DirectByteBuffer2_NeverZero_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBuffer2CoverageMap.class); staticMethod_Unsafe_NeverZero_sanitize = instrumentWithStrategy(new StaticMethodStrategy(), UnsafeCoverageMap.class); + staticMethod_Unsafe_NeverZero2_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), Unsafe2CoverageMap.class); + staticMethod_Unsafe_SimpleIncrement_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), UnsafeSimpleIncrementCoverageMap.class); + staticMethod_Unsafe_NeverZeroBranchfree_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), UnsafeBranchfreeCoverageMap.class); } @Benchmark @@ -101,10 +113,30 @@ public class CoverageInstrumentationBenchmark { return (String) staticMethod_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG); } + @Benchmark + public String staticMethod_DirectByteBuffer2_NeverZero() throws Throwable { + return (String) staticMethod_DirectByteBuffer2_NeverZero_sanitize.invokeExact(TARGET_ARG); + } + @Benchmark public String staticMethod_Unsafe_NeverZero() throws Throwable { return (String) staticMethod_Unsafe_NeverZero_sanitize.invokeExact(TARGET_ARG); } + + @Benchmark + public String staticMethod_Unsafe_NeverZero2() throws Throwable { + return (String) staticMethod_Unsafe_NeverZero2_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_Unsafe_SimpleIncrement() throws Throwable { + return (String) staticMethod_Unsafe_SimpleIncrement_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_Unsafe_NeverZeroBranchfree() throws Throwable { + return (String) staticMethod_Unsafe_NeverZeroBranchfree_sanitize.invokeExact(TARGET_ARG); + } } class InstrumentingClassLoader extends ClassLoader { diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBuffer2CoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBuffer2CoverageMap.java new file mode 100644 index 00000000..c57babb5 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBuffer2CoverageMap.java @@ -0,0 +1,32 @@ +// Copyright 2022 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.nio.ByteBuffer; + +public final class DirectByteBuffer2CoverageMap { + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final int NUM_COUNTERS = 4096; + public static final ByteBuffer counters = ByteBuffer.allocateDirect(NUM_COUNTERS); + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final byte counter = counters.get(id); + counters.put(id, (byte) (counter == -1 ? 1 : counter + 1)); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/Unsafe2CoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/Unsafe2CoverageMap.java new file mode 100644 index 00000000..030d9a95 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/Unsafe2CoverageMap.java @@ -0,0 +1,55 @@ +// Copyright 2022 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.reflect.Field; +import sun.misc.Unsafe; + +public final class Unsafe2CoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + final byte counter = UNSAFE.getByte(address); + UNSAFE.putByte(address, (byte) (counter == -1 ? 1 : counter + 1)); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeBranchfreeCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeBranchfreeCoverageMap.java new file mode 100644 index 00000000..3694b95f --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeBranchfreeCoverageMap.java @@ -0,0 +1,55 @@ +// Copyright 2022 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.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeBranchfreeCoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + final int incrementedCounter = UNSAFE.getByte(address) + 1; + UNSAFE.putByte(address, (byte) (incrementedCounter ^ (incrementedCounter >>> 8))); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeSimpleIncrementCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeSimpleIncrementCoverageMap.java new file mode 100644 index 00000000..60fb8c8d --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeSimpleIncrementCoverageMap.java @@ -0,0 +1,54 @@ +// Copyright 2022 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.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeSimpleIncrementCoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + UNSAFE.putByte(address, (byte) (UNSAFE.getByte(address) + 1)); + } +} -- cgit v1.2.3 From 70f4db399d9b5e7e9b1910bde403e67ee9857e91 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 8 Feb 2022 12:44:28 +0100 Subject: Switch to an Unsafe-backed CoverageMap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmarks showed that this instrumentation, which has the name staticMethod_Unsafe_NeverZero2, is almost 2.5x faster than the inlined DirectByteBuffer strategy: CoverageInstrumentationBenchmark.local_DirectByteBuffer_NeverZero thrpt 25 606359.097 ± 51464.275 ops/s CoverageInstrumentationBenchmark.staticMethod_DirectByteBuffer2_NeverZero thrpt 25 848459.679 ± 127822.610 ops/s CoverageInstrumentationBenchmark.staticMethod_DirectByteBuffer_NeverZero thrpt 25 806509.876 ± 131400.478 ops/s CoverageInstrumentationBenchmark.staticMethod_Unsafe_AtomicSimpleIncrement thrpt 25 258652.693 ± 994.936 ops/s CoverageInstrumentationBenchmark.staticMethod_Unsafe_NeverZero thrpt 25 1393052.150 ± 11020.556 ops/s CoverageInstrumentationBenchmark.staticMethod_Unsafe_NeverZero2 thrpt 25 1477061.469 ± 7437.856 ops/s CoverageInstrumentationBenchmark.staticMethod_Unsafe_NeverZeroBranchfree thrpt 25 1269241.908 ± 11487.180 ops/s CoverageInstrumentationBenchmark.staticMethod_Unsafe_SimpleIncrement thrpt 25 1716329.628 ± 33942.194 ops/s CoverageInstrumentationBenchmark.uninstrumented thrpt 25 3194175.390 ± 65778.337 ops/s This commit preserves the original strategy as part of the benchmark. --- .../jazzer/instrumentor/BUILD.bazel | 14 +++- .../instrumentor/DirectByteBufferStrategy.kt | 81 ++++++++++++++++++++++ .../jazzer/instrumentor/StaticMethodStrategy.java | 48 ------------- 3 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt delete mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index e8858f0f..fd3a8ae1 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -1,3 +1,5 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + java_binary( name = "CoverageInstrumentationBenchmark", main_class = "org.openjdk.jmh.Main", @@ -37,6 +39,7 @@ java_library( "@maven//:com_mikesamuel_json_sanitizer", ], deps = [ + ":kotlin_strategies", ":strategies", "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", "@maven//:org_openjdk_jmh_jmh_core", @@ -48,7 +51,6 @@ java_library( srcs = [ "DirectByteBuffer2CoverageMap.java", "DirectByteBufferCoverageMap.java", - "StaticMethodStrategy.java", "Unsafe2CoverageMap.java", "UnsafeBranchfreeCoverageMap.java", "UnsafeCoverageMap.java", @@ -61,6 +63,16 @@ java_library( ], ) +kt_jvm_library( + name = "kotlin_strategies", + srcs = ["DirectByteBufferStrategy.kt"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@jazzer_jacoco//:jacoco_internal", + "@jazzer_ow2_asm//:asm", + ], +) + java_plugin( name = "JmhGeneratorAnnotationProcessor", processor_class = "org.openjdk.jmh.generators.BenchmarkProcessor", diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt new file mode 100644 index 00000000..49090184 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt @@ -0,0 +1,81 @@ +// Copyright 2022 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 org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +object DirectByteBufferStrategy : EdgeCoverageStrategy { + + override fun instrumentControlFlowEdge( + mv: MethodVisitor, + edgeId: Int, + variable: Int, + coverageMapInternalClassName: String + ) { + mv.apply { + visitVarInsn(Opcodes.ALOAD, variable) + // Stack: counters + push(edgeId) + // Stack: counters | edgeId + visitInsn(Opcodes.DUP2) + // Stack: counters | edgeId | counters | edgeId + visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "get", "(I)B", false) + // Increment the counter, but ensure that it never stays at 0 after an overflow by incrementing it again in + // that case. + // This approach performs better than saturating the counter at 255 (see Section 3.3 of + // https://www.usenix.org/system/files/woot20-paper-fioraldi.pdf) + // Stack: counters | edgeId | counter (sign-extended to int) + push(0xff) + // Stack: counters | edgeId | counter (sign-extended to int) | 0x000000ff + visitInsn(Opcodes.IAND) + // Stack: counters | edgeId | counter (zero-extended to int) + push(1) + // Stack: counters | edgeId | counter | 1 + visitInsn(Opcodes.IADD) + // Stack: counters | edgeId | counter + 1 + visitInsn(Opcodes.DUP) + // Stack: counters | edgeId | counter + 1 | counter + 1 + push(8) + // Stack: counters | edgeId | counter + 1 | counter + 1 | 8 (maxStack: +5) + visitInsn(Opcodes.ISHR) + // Stack: counters | edgeId | counter + 1 | 1 if the increment overflowed to 0, 0 otherwise + visitInsn(Opcodes.IADD) + // Stack: counters | edgeId | counter + 2 if the increment overflowed, counter + 1 otherwise + visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "put", "(IB)Ljava/nio/ByteBuffer;", false) + // Stack: counters + visitInsn(Opcodes.POP) + } + } + + override val instrumentControlFlowEdgeStackSize = 5 + + override val localVariableType get() = "java/nio/ByteBuffer" + + override fun loadLocalVariable(mv: MethodVisitor, variable: Int, coverageMapInternalClassName: String) { + mv.apply { + visitFieldInsn( + Opcodes.GETSTATIC, + coverageMapInternalClassName, + "counters", + "Ljava/nio/ByteBuffer;", + ) + // Stack: counters (maxStack: 1) + visitVarInsn(Opcodes.ASTORE, variable) + } + } + + override val loadLocalVariableStackSize = 1 +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java deleted file mode 100644 index ca0bd3d7..00000000 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022 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 org.jacoco.core.internal.instr.InstrSupport; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - -public class StaticMethodStrategy implements EdgeCoverageStrategy { - @Override - public void instrumentControlFlowEdge( - MethodVisitor mv, int edgeId, int variable, String coverageMapInternalClassName) { - InstrSupport.push(mv, edgeId); - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, coverageMapInternalClassName, "recordCoverage", "(I)V", false); - } - - @Override - public int getInstrumentControlFlowEdgeStackSize() { - return 1; - } - - @Override - public Object getLocalVariableType() { - return null; - } - - @Override - public void loadLocalVariable( - MethodVisitor mv, int variable, String coverageMapInternalClassName) {} - - @Override - public int getLoadLocalVariableStackSize() { - return 0; - } -} -- cgit v1.2.3 From dfa07a8faf76e6d7d4e5f2c9616665234e58f683 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 7 Feb 2022 12:50:40 +0100 Subject: Get ASM from Maven rather than gitlab.ow2.org The OW2 GitLab hasn't been very reliable in the past and just encountered another outage. Getting the ASM jars from Maven should be more reliable. --- .../jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index fd3a8ae1..8862c435 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -59,7 +59,7 @@ java_library( deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", "@jazzer_jacoco//:jacoco_internal", - "@jazzer_ow2_asm//:asm", + "@org_ow2_asm_asm//jar", ], ) @@ -69,7 +69,7 @@ kt_jvm_library( deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", "@jazzer_jacoco//:jacoco_internal", - "@jazzer_ow2_asm//:asm", + "@org_ow2_asm_asm//jar", ], ) -- cgit v1.2.3 From 23b08b6b64155621386967e089021fa75983f59c Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 21 Feb 2022 09:58:40 +0100 Subject: Add benchmarks for fuzzer callbacks The parts of the benchmark that require a JDK supporting Project Panama are commented out as there is no good way to enable them only if the current JDK supports them. --- .../java/com/code_intelligence/jazzer/BUILD.bazel | 6 + .../jazzer/instrumentor/BUILD.bazel | 20 +- .../jmh/java/com/code_intelligence/jazzer/jmh.bzl | 24 +++ .../code_intelligence/jazzer/runtime/BUILD.bazel | 50 +++++ .../jazzer/runtime/FuzzerCallbacks.java | 29 +++ .../jazzer/runtime/FuzzerCallbacksBenchmark.java | 219 +++++++++++++++++++++ .../runtime/FuzzerCallbacksOptimizedCritical.java | 46 +++++ .../FuzzerCallbacksOptimizedNonCritical.java | 46 +++++ .../jazzer/runtime/FuzzerCallbacksPanama.java | 59 ++++++ .../jazzer/runtime/FuzzerCallbacksWithPc.java | 31 +++ 10 files changed, 513 insertions(+), 17 deletions(-) create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/BUILD.bazel create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/runtime/BUILD.bazel create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacks.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksBenchmark.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedCritical.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedNonCritical.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksPanama.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksWithPc.java (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/BUILD.bazel new file mode 100644 index 00000000..cf6acfbc --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/BUILD.bazel @@ -0,0 +1,6 @@ +java_plugin( + name = "JmhGeneratorAnnotationProcessor", + processor_class = "org.openjdk.jmh.generators.BenchmarkProcessor", + visibility = ["//agent/src/jmh/java:__subpackages__"], + deps = ["@maven//:org_openjdk_jmh_jmh_generator_annprocess"], +) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 8862c435..f38cbc6a 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -1,4 +1,5 @@ load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("//agent/src/jmh/java/com/code_intelligence/jazzer:jmh.bzl", "JMH_TEST_ARGS") java_binary( name = "CoverageInstrumentationBenchmark", @@ -10,16 +11,7 @@ java_binary( java_test( name = "CoverageInstrumentationBenchmarkTest", - args = [ - # Fail fast on any exceptions produced by benchmarks. - "-foe true", - "-wf 1", - "-f 1", - "-wi 1", - "-i 1", - "-r 1s", - "-w 1s", - ], + args = JMH_TEST_ARGS, jvm_flags = [ "-XX:CompileCommand=print,*CoverageMap.recordCoverage", ], @@ -34,7 +26,7 @@ java_test( java_library( name = "coverage_instrumentation_benchmark", srcs = ["CoverageInstrumentationBenchmark.java"], - plugins = [":JmhGeneratorAnnotationProcessor"], + plugins = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], runtime_deps = [ "@maven//:com_mikesamuel_json_sanitizer", ], @@ -72,9 +64,3 @@ kt_jvm_library( "@org_ow2_asm_asm//jar", ], ) - -java_plugin( - name = "JmhGeneratorAnnotationProcessor", - processor_class = "org.openjdk.jmh.generators.BenchmarkProcessor", - deps = ["@maven//:org_openjdk_jmh_jmh_generator_annprocess"], -) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl new file mode 100644 index 00000000..5391a46b --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl @@ -0,0 +1,24 @@ +# Copyright 2022 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. + +JMH_TEST_ARGS = [ + # Fail fast on any exceptions produced by benchmarks. + "-foe true", + "-wf 0", + "-f 1", + "-wi 0", + "-i 1", + "-r 1s", + "-w 1s", +] diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..96fd8e1f --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,50 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") +load("//agent/src/jmh/java/com/code_intelligence/jazzer:jmh.bzl", "JMH_TEST_ARGS") + +java_binary( + name = "FuzzerCallbacksBenchmark", + main_class = "org.openjdk.jmh.Main", + runtime_deps = [ + ":fuzzer_callbacks_benchmark", + ], +) + +java_test( + name = "FuzzerCallbacksBenchmarkTest", + args = JMH_TEST_ARGS, + main_class = "org.openjdk.jmh.Main", + # Directly invoke JMH's main without using a testrunner. + use_testrunner = False, + runtime_deps = [ + ":fuzzer_callbacks_benchmark", + ], +) + +java_library( + name = "fuzzer_callbacks_benchmark", + srcs = ["FuzzerCallbacksBenchmark.java"], + plugins = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], + deps = [ + ":fuzzer_callbacks", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) + +java_jni_library( + name = "fuzzer_callbacks", + srcs = [ + "FuzzerCallbacks.java", + "FuzzerCallbacksOptimizedCritical.java", + "FuzzerCallbacksOptimizedNonCritical.java", + # Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + # "FuzzerCallbacksPanama.java", + "FuzzerCallbacksWithPc.java", + ], + javacopts = [ + # Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + # "--add-modules", + # "jdk.incubator.foreign", + ], + native_libs = ["//agent/src/jmh/native/com/code_intelligence/jazzer/runtime:fuzzer_callbacks"], + visibility = ["//agent/src/jmh/native/com/code_intelligence/jazzer/runtime:__pkg__"], +) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacks.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacks.java new file mode 100644 index 00000000..6e8343ce --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacks.java @@ -0,0 +1,29 @@ +// Copyright 2022 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.github.fmeum.rules_jni.RulesJni; + +public final class FuzzerCallbacks { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacks.class); + } + + static native void traceCmpInt(int arg1, int arg2, int pc); + static native void traceSwitch(long val, long[] cases, int pc); + + static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + static native void traceStrstr(String s1, String s2, int pc); +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksBenchmark.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksBenchmark.java new file mode 100644 index 00000000..b55a9936 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksBenchmark.java @@ -0,0 +1,219 @@ +// Copyright 2022 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 java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 3) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class FuzzerCallbacksBenchmark { + @State(Scope.Benchmark) + public static class TraceCmpIntState { + int arg1 = 0xCAFECAFE; + int arg2 = 0xFEEDFEED; + int pc = 0x12345678; + } + + @Benchmark + public void traceCmpInt(TraceCmpIntState state) { + FuzzerCallbacks.traceCmpInt(state.arg1, state.arg2, state.pc); + } + + @Benchmark + public void traceCmpIntWithPc(TraceCmpIntState state) { + FuzzerCallbacksWithPc.traceCmpInt(state.arg1, state.arg2, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceCmpIntOptimizedCritical(TraceCmpIntState state) { + FuzzerCallbacksOptimizedCritical.traceCmpInt(state.arg1, state.arg2, state.pc); + } + + // Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + // @Benchmark + // @Fork(jvmArgsAppend = {"--enable-native-access=ALL-UNNAMED", "--add-modules", + // "jdk.incubator.foreign"}) + // public void + // traceCmpIntPanama(TraceCmpIntState state) throws Throwable { + // FuzzerCallbacksPanama.traceCmpInt(state.arg1, state.arg2, state.pc); + // } + + @State(Scope.Benchmark) + public static class TraceSwitchState { + @Param({"5", "10"}) int numCases; + + long val; + long[] cases; + int pc = 0x12345678; + + @Setup + public void setup() { + cases = new long[2 + numCases]; + Random random = ThreadLocalRandom.current(); + Arrays.setAll(cases, i -> { + switch (i) { + case 0: + return numCases; + case 1: + return 32; + default: + return random.nextInt(); + } + }); + Arrays.sort(cases, 2, cases.length); + val = random.nextInt(); + } + } + + @Benchmark + public void traceSwitch(TraceSwitchState state) { + FuzzerCallbacks.traceSwitch(state.val, state.cases, state.pc); + } + + @Benchmark + public void traceSwitchWithPc(TraceSwitchState state) { + FuzzerCallbacksWithPc.traceSwitch(state.val, state.cases, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceSwitchOptimizedCritical(TraceSwitchState state) { + FuzzerCallbacksOptimizedCritical.traceSwitch(state.val, state.cases, state.pc); + } + + @Benchmark + public void traceSwitchOptimizedNonCritical(TraceSwitchState state) { + FuzzerCallbacksOptimizedNonCritical.traceSwitch(state.val, state.cases, state.pc); + } + + // Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + // @Benchmark + // @Fork(jvmArgsAppend = {"--enable-native-access=ALL-UNNAMED", "--add-modules", + // "jdk.incubator.foreign"}) + // public void + // traceCmpSwitchPanama(TraceSwitchState state) throws Throwable { + // FuzzerCallbacksPanama.traceCmpSwitch(state.val, state.cases, state.pc); + // } + + @State(Scope.Benchmark) + public static class TraceMemcmpState { + @Param({"10", "100", "1000"}) int length; + + byte[] array1; + byte[] array2; + int pc = 0x12345678; + + @Setup + public void setup() { + array1 = new byte[length]; + array2 = new byte[length]; + + Random random = ThreadLocalRandom.current(); + random.nextBytes(array1); + random.nextBytes(array2); + // Make the arrays agree unil the midpoint to benchmark the "average" + // case of an interesting memcmp. + System.arraycopy(array1, 0, array2, 0, length / 2); + } + } + + @Benchmark + public void traceMemcmp(TraceMemcmpState state) { + FuzzerCallbacks.traceMemcmp(state.array1, state.array2, 1, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceMemcmpOptimizedCritical(TraceMemcmpState state) { + FuzzerCallbacksOptimizedCritical.traceMemcmp(state.array1, state.array2, 1, state.pc); + } + + @Benchmark + public void traceMemcmpOptimizedNonCritical(TraceMemcmpState state) { + FuzzerCallbacksOptimizedNonCritical.traceMemcmp(state.array1, state.array2, 1, state.pc); + } + + @State(Scope.Benchmark) + public static class TraceStrstrState { + @Param({"10", "100", "1000"}) int length; + @Param({"true", "false"}) boolean asciiOnly; + + String haystack; + String needle; + int pc = 0x12345678; + + @Setup + public void setup() { + haystack = randomString(length, asciiOnly); + needle = randomString(length, asciiOnly); + } + + private String randomString(int length, boolean asciiOnly) { + String asciiString = + ThreadLocalRandom.current() + .ints('a', 'z' + 1) + .limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + if (asciiOnly) { + return asciiString; + } + // Force String to be non-Latin-1 to preclude compact string optimization. + return "\uFFFD" + asciiString.substring(1); + } + } + + @Benchmark + public void traceStrstr(TraceStrstrState state) { + FuzzerCallbacks.traceStrstr(state.haystack, state.needle, state.pc); + } + + @Benchmark + public void traceStrstrOptimizedNonCritical(TraceStrstrState state) { + FuzzerCallbacksOptimizedNonCritical.traceStrstr(state.haystack, state.needle, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceStrstrOptimizedJavaCritical(TraceStrstrState state) + throws UnsupportedEncodingException { + FuzzerCallbacksOptimizedCritical.traceStrstrJava(state.haystack, state.needle, state.pc); + } + + @Benchmark + public void traceStrstrOptimizedJavaNonCritical(TraceStrstrState state) + throws UnsupportedEncodingException { + FuzzerCallbacksOptimizedNonCritical.traceStrstrJava(state.haystack, state.needle, state.pc); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedCritical.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedCritical.java new file mode 100644 index 00000000..1c09e9ad --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedCritical.java @@ -0,0 +1,46 @@ +// Copyright 2022 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.github.fmeum.rules_jni.RulesJni; +import java.io.UnsupportedEncodingException; + +/** + * Optimized implementations of the libFuzzer callbacks that do rely on the deprecated + * CriticalJNINatives feature. Methods with `Java` in their name implement some parts in Java. + */ +public final class FuzzerCallbacksOptimizedCritical { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacksOptimizedCritical.class); + } + + static native void traceCmpInt(int arg1, int arg2, int pc); + + static native void traceSwitch(long val, long[] cases, int pc); + + static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + + static void traceStrstrJava(String haystack, String needle, int pc) + throws UnsupportedEncodingException { + // Note that we are not encoding as modified UTF-8 here: The FuzzedDataProvider transparently + // converts CESU8 into modified UTF-8 by coding null bytes on two bytes. Since the fuzzer is + // more likely to insert literal null bytes, having both the fuzzer input and the reported + // string comparisons be CESU8 should perform even better than the current implementation using + // modified UTF-8. + traceStrstrInternal(needle.substring(0, Math.min(needle.length(), 64)).getBytes("CESU8"), pc); + } + + private static native void traceStrstrInternal(byte[] needle, int pc); +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedNonCritical.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedNonCritical.java new file mode 100644 index 00000000..25fad3bf --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedNonCritical.java @@ -0,0 +1,46 @@ +// Copyright 2022 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.github.fmeum.rules_jni.RulesJni; +import java.io.UnsupportedEncodingException; + +/** + * Optimized implementations of the libFuzzer callbacks that do not rely on the deprecated + * CriticalJNINatives feature. Methods with `Java` in their name implement some parts in Java. + */ +public final class FuzzerCallbacksOptimizedNonCritical { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacksOptimizedNonCritical.class); + } + + static native void traceSwitch(long val, long[] cases, int pc); + + static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + + static native void traceStrstr(String s1, String s2, int pc); + + static void traceStrstrJava(String haystack, String needle, int pc) + throws UnsupportedEncodingException { + // Note that we are not encoding as modified UTF-8 here: The FuzzedDataProvider transparently + // converts CESU8 into modified UTF-8 by coding null bytes on two bytes. Since the fuzzer is + // more likely to insert literal null bytes, having both the fuzzer input and the reported + // string comparisons be CESU8 should perform even better than the current implementation using + // modified UTF-8. + traceStrstrInternal(needle.substring(0, Math.min(needle.length(), 64)).getBytes("CESU8"), pc); + } + + private static native void traceStrstrInternal(byte[] needle, int pc); +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksPanama.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksPanama.java new file mode 100644 index 00000000..ce3d6290 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksPanama.java @@ -0,0 +1,59 @@ +// Copyright 2022 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.github.fmeum.rules_jni.RulesJni; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import jdk.incubator.foreign.CLinker; +import jdk.incubator.foreign.FunctionDescriptor; +import jdk.incubator.foreign.MemoryAddress; +import jdk.incubator.foreign.MemoryLayout; +import jdk.incubator.foreign.MemorySegment; +import jdk.incubator.foreign.ResourceScope; +import jdk.incubator.foreign.SymbolLookup; + +/** + * Pure-Java implementation of the fuzzer callbacks backed by Project Panama (requires JDK 16+). + * To include the implementation in the benchmark on a supported JDK, uncomment the relevant lines + * in BUILD.bazel. + */ +public class FuzzerCallbacksPanama { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacks.class); + } + + private static final MethodHandle traceCmp4 = CLinker.getInstance().downcallHandle( + SymbolLookup.loaderLookup().lookup("__sanitizer_cov_trace_cmp4").get(), + MethodType.methodType(void.class, int.class, int.class), + FunctionDescriptor.ofVoid(CLinker.C_INT, CLinker.C_INT)); + private static final MethodHandle traceSwitch = CLinker.getInstance().downcallHandle( + SymbolLookup.loaderLookup().lookup("__sanitizer_cov_trace_switch").get(), + MethodType.methodType(void.class, long.class, MemoryAddress.class), + FunctionDescriptor.ofVoid(CLinker.C_LONG, CLinker.C_POINTER)); + + static void traceCmpInt(int arg1, int arg2, int pc) throws Throwable { + traceCmp4.invokeExact(arg1, arg2); + } + + static void traceCmpSwitch(long val, long[] cases, int pc) throws Throwable { + try (ResourceScope scope = ResourceScope.newConfinedScope()) { + MemorySegment nativeCopy = MemorySegment.allocateNative( + MemoryLayout.sequenceLayout(cases.length, CLinker.C_LONG), scope); + nativeCopy.copyFrom(MemorySegment.ofArray(cases)); + traceSwitch.invokeExact(val, nativeCopy.address()); + } + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksWithPc.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksWithPc.java new file mode 100644 index 00000000..21f416cf --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksWithPc.java @@ -0,0 +1,31 @@ +// Copyright 2022 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.github.fmeum.rules_jni.RulesJni; + +/** + * Unoptimized implementation of the libFuzzer callbacks that use the trampoline construction to + * inject fake PCs. + */ +public final class FuzzerCallbacksWithPc { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacksWithPc.class); + } + + static native void traceCmpInt(int arg1, int arg2, int pc); + + static native void traceSwitch(long val, long[] cases, int pc); +} -- cgit v1.2.3 From c276a45046a93342e1924d5f14bc5d25d2aa8f00 Mon Sep 17 00:00:00 2001 From: Norbert Schneider Date: Wed, 6 Apr 2022 08:47:22 +0200 Subject: Use the official JaCoCo version Switch from the internal fork to the official JaCoCo version. This looses the call optimizations but removes the burden of maintaining a dedicated fork. Tests using the example fuzzers and JMH don't show huge performance differences. Some are more in favor of the fork, some of the official version. --- .../jazzer/instrumentor/BUILD.bazel | 37 +++++++++++ .../instrumentor/EdgeCoverageInstrumentation.java | 74 ++++++++++++++++++++++ .../jazzer/instrumentor/EdgeCoverageTarget.java | 44 +++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java create mode 100644 agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageTarget.java (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index f38cbc6a..ada825f1 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -1,3 +1,4 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library", "java_jni_library") load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") load("//agent/src/jmh/java/com/code_intelligence/jazzer:jmh.bzl", "JMH_TEST_ARGS") @@ -64,3 +65,39 @@ kt_jvm_library( "@org_ow2_asm_asm//jar", ], ) + +java_binary( + name = "EdgeCoverageInstrumentationBenchmark", + main_class = "org.openjdk.jmh.Main", + runtime_deps = [ + ":edge_coverage_instrumentation_benchmark", + ], +) + +java_test( + name = "EdgeCoverageInstrumentationBenchmarkTest", + args = JMH_TEST_ARGS, + main_class = "org.openjdk.jmh.Main", + # Directly invoke JMH's main without using a testrunner. + use_testrunner = False, + runtime_deps = [ + ":edge_coverage_instrumentation_benchmark", + ], +) + +java_jni_library( + name = "edge_coverage_instrumentation_benchmark", + srcs = [ + "EdgeCoverageInstrumentation.java", + "EdgeCoverageTarget.java", + ], + native_libs = ["//driver:coverage_tracker_jni"], + plugins = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:coverage_map", + "//agent/src/test/java/com/code_intelligence/jazzer:MockDriver", + "//agent/src/test/java/com/code_intelligence/jazzer/instrumentor:patch_test_utils", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java new file mode 100644 index 00000000..c2c2697d --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java @@ -0,0 +1,74 @@ +// Copyright 2022 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 static com.code_intelligence.jazzer.instrumentor.PatchTestUtils.*; +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodType.methodType; + +import com.code_intelligence.jazzer.MockDriver; +import com.code_intelligence.jazzer.runtime.CoverageMap; +import com.github.fmeum.rules_jni.RulesJni; +import java.io.*; +import java.lang.invoke.*; +import java.nio.file.Files; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; + +@Warmup(iterations = 10, time = 3) +@Measurement(iterations = 10, time = 3) +@Fork(value = 3) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +@SuppressWarnings("unused") +public class EdgeCoverageInstrumentation { + private MethodHandle exampleMethod; + + static { + MockDriver.load(); + RulesJni.loadLibrary("coverage_tracker_jni", "/driver"); + } + + @Setup + public void setupInstrumentation() throws Throwable { + String outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"); + if (outDir == null || outDir.isEmpty()) { + outDir = + Files.createTempDirectory(EdgeCoverageInstrumentation.class.getSimpleName()).toString(); + } + + byte[] originalBytecode = classToBytecode(EdgeCoverageTarget.class); + dumpBytecode(outDir, EdgeCoverageTarget.class.getName(), originalBytecode); + + byte[] patchedBytecode = applyInstrumentation(originalBytecode); + dumpBytecode(outDir, EdgeCoverageTarget.class.getName() + ".patched", patchedBytecode); + + Class patchedClass = bytecodeToClass(EdgeCoverageTarget.class.getName(), patchedBytecode); + Object obj = lookup().findConstructor(patchedClass, methodType(void.class)).invoke(); + exampleMethod = lookup().bind(obj, "exampleMethod", methodType(List.class)); + } + + private byte[] applyInstrumentation(byte[] bytecode) { + return new EdgeCoverageInstrumentor(new StaticMethodStrategy(), CoverageMap.class, 0) + .instrument(bytecode); + } + + @Benchmark + public Object benchmarkInstrumentedMethodCall() throws Throwable { + return exampleMethod.invoke(); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageTarget.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageTarget.java new file mode 100644 index 00000000..57eb8807 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageTarget.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 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.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +public class EdgeCoverageTarget { + private final Random rnd = new Random(); + + @SuppressWarnings("unused") + public List exampleMethod() { + ArrayList rnds = new ArrayList<>(); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + int i = rnd.nextInt() + rnd.nextInt(); + if (i > 0 && i < Integer.MAX_VALUE / 2) { + i--; + } else { + i++; + } + rnds.add(i); + return rnds.stream().map(n -> n + 1).collect(Collectors.toList()); + } +} -- cgit v1.2.3 From b5aab47852542bc4ba55c61127f645bde69c569f Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 9 Aug 2022 11:30:08 +0200 Subject: all: Refactor Jazzer driver into a JNI shared library For the case of Java-only fuzzing, which only requires the JNI shared library and no driver binary, this change is a pure refactoring. Fuzzing native libraries requires some structural changes since loading libFuzzer from a shared library has implications on the behavior of the dynamic linker: 1. __sanitizer_set_death_callback now has to be looked up via dlsym since it isn't contained in the same object as libFuzzer itself anymore. 2. All sanitizer hooks and libc functions to hook have to be defined in the driver executable. They delegate to the real hooks defined in the shared library as soon as it has been loaded. --- .../java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel | 3 +-- .../jazzer/instrumentor/EdgeCoverageInstrumentation.java | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) (limited to 'agent/src/jmh/java') diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index ada825f1..fe68f903 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -91,12 +91,11 @@ java_jni_library( "EdgeCoverageInstrumentation.java", "EdgeCoverageTarget.java", ], - native_libs = ["//driver:coverage_tracker_jni"], + native_libs = ["//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"], plugins = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", "//agent/src/main/java/com/code_intelligence/jazzer/runtime:coverage_map", - "//agent/src/test/java/com/code_intelligence/jazzer:MockDriver", "//agent/src/test/java/com/code_intelligence/jazzer/instrumentor:patch_test_utils", "@maven//:org_openjdk_jmh_jmh_core", ], diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java index c2c2697d..e2eeadd3 100644 --- a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java @@ -18,10 +18,7 @@ import static com.code_intelligence.jazzer.instrumentor.PatchTestUtils.*; import static java.lang.invoke.MethodHandles.lookup; import static java.lang.invoke.MethodType.methodType; -import com.code_intelligence.jazzer.MockDriver; import com.code_intelligence.jazzer.runtime.CoverageMap; -import com.github.fmeum.rules_jni.RulesJni; -import java.io.*; import java.lang.invoke.*; import java.nio.file.Files; import java.util.List; @@ -38,11 +35,6 @@ import org.openjdk.jmh.annotations.*; public class EdgeCoverageInstrumentation { private MethodHandle exampleMethod; - static { - MockDriver.load(); - RulesJni.loadLibrary("coverage_tracker_jni", "/driver"); - } - @Setup public void setupInstrumentation() throws Throwable { String outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"); -- cgit v1.2.3