diff options
Diffstat (limited to 'agent/src')
95 files changed, 4598 insertions, 1309 deletions
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 new file mode 100644 index 00000000..fe68f903 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -0,0 +1,102 @@ +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") + +java_binary( + name = "CoverageInstrumentationBenchmark", + main_class = "org.openjdk.jmh.Main", + runtime_deps = [ + ":coverage_instrumentation_benchmark", + ], +) + +java_test( + name = "CoverageInstrumentationBenchmarkTest", + args = JMH_TEST_ARGS, + 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 = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], + runtime_deps = [ + "@maven//:com_mikesamuel_json_sanitizer", + ], + deps = [ + ":kotlin_strategies", + ":strategies", + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) + +java_library( + name = "strategies", + srcs = [ + "DirectByteBuffer2CoverageMap.java", + "DirectByteBufferCoverageMap.java", + "Unsafe2CoverageMap.java", + "UnsafeBranchfreeCoverageMap.java", + "UnsafeCoverageMap.java", + "UnsafeSimpleIncrementCoverageMap.java", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@jazzer_jacoco//:jacoco_internal", + "@org_ow2_asm_asm//jar", + ], +) + +kt_jvm_library( + name = "kotlin_strategies", + srcs = ["DirectByteBufferStrategy.kt"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@jazzer_jacoco//:jacoco_internal", + "@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/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/instrumentor:patch_test_utils", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) 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..f388c4cc --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java @@ -0,0 +1,178 @@ +// 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; + 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 { + 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); + 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 + 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); + } + + @Benchmark + public String staticMethod_DirectByteBuffer_NeverZero() throws Throwable { + 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 { + 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/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/DirectByteBufferCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java new file mode 100644 index 00000000..e5e66abb --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java @@ -0,0 +1,36 @@ +// 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. + } + + 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/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/EdgeCoverageInstrumentation.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java new file mode 100644 index 00000000..e2eeadd3 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java @@ -0,0 +1,66 @@ +// 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.runtime.CoverageMap; +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; + + @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<Integer> exampleMethod() { + ArrayList<Integer> 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()); + } +} 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/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)); + } + } +} 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)); + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl index 1463c602..5391a46b 100755..100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl @@ -1,5 +1,4 @@ -#!/usr/bin/env sh -# Copyright 2021 Code Intelligence GmbH +# 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. @@ -13,6 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -e -bazel build //agent/src/main/java/com/code_intelligence/jazzer/generated:java_no_throw_methods_list -cp bazel-bin/agent/src/main/java/com/code_intelligence/jazzer/generated/java_no_throw_methods_list.dat.generated agent/src/main/java/com/code_intelligence/jazzer/generated/java_no_throw_methods_list.dat +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); +} diff --git a/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..33a03036 --- /dev/null +++ b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,12 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") + +cc_jni_library( + name = "fuzzer_callbacks", + srcs = ["fuzzer_callbacks.cpp"], + visibility = ["//agent/src/jmh/java/com/code_intelligence/jazzer/runtime:__pkg__"], + deps = [ + "//agent/src/jmh/java/com/code_intelligence/jazzer/runtime:fuzzer_callbacks.hdrs", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:sanitizer_hooks_with_pc", + "@jazzer_libfuzzer//:libfuzzer_no_main", + ], +) diff --git a/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/fuzzer_callbacks.cpp b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/fuzzer_callbacks.cpp new file mode 100644 index 00000000..2562db1f --- /dev/null +++ b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/fuzzer_callbacks.cpp @@ -0,0 +1,213 @@ +// 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. + +#include <jni.h> + +#include <cstddef> +#include <cstdint> + +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacks.h" +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical.h" +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical.h" +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacksWithPc.h" +#include "driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_hooks_with_pc.h" + +extern "C" { +void __sanitizer_weak_hook_compare_bytes(void *caller_pc, const void *s1, + const void *s2, std::size_t n1, + std::size_t n2, int result); +void __sanitizer_weak_hook_strstr(void *caller_pc, const char *s1, + const char *s2, const char *result); +void __sanitizer_weak_hook_memmem(void *caller_pc, const void *b1, + std::size_t n1, const void *s2, + std::size_t n2, void *result); +void __sanitizer_cov_trace_cmp4(uint32_t arg1, uint32_t arg2); +void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2); + +void __sanitizer_cov_trace_switch(uint64_t val, uint64_t *cases); + +void __sanitizer_cov_trace_div4(uint32_t val); +void __sanitizer_cov_trace_div8(uint64_t val); + +void __sanitizer_cov_trace_gep(uintptr_t idx); +} + +inline __attribute__((always_inline)) void *idToPc(jint id) { + return reinterpret_cast<void *>(static_cast<uintptr_t>(id)); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4(value1, value2); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksWithPc_traceCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4_with_pc(idToPc(id), value1, value2); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4(value1, value2); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceCmpInt( + jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4(value1, value2); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + jlong *case_values = + env->GetLongArrayElements(libfuzzer_case_values, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + __sanitizer_cov_trace_switch(switch_value, + reinterpret_cast<uint64_t *>(case_values)); + env->ReleaseLongArrayElements(libfuzzer_case_values, case_values, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + auto *case_values = static_cast<jlong *>( + env->GetPrimitiveArrayCritical(libfuzzer_case_values, nullptr)); + __sanitizer_cov_trace_switch(switch_value, + reinterpret_cast<uint64_t *>(case_values)); + env->ReleasePrimitiveArrayCritical(libfuzzer_case_values, case_values, + JNI_ABORT); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksWithPc_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + jlong *case_values = + env->GetLongArrayElements(libfuzzer_case_values, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + __sanitizer_cov_trace_switch_with_pc( + idToPc(id), switch_value, reinterpret_cast<uint64_t *>(case_values)); + env->ReleaseLongArrayElements(libfuzzer_case_values, case_values, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceSwitch( + env, cls, switch_value, libfuzzer_case_values, id); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceSwitch( + jlong switch_value, jint case_values_length, jlong *case_values, jint id) { + __sanitizer_cov_trace_switch(switch_value, + reinterpret_cast<uint64_t *>(case_values)); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceMemcmp( + JNIEnv *env, jclass cls, jbyteArray b1, jbyteArray b2, jint result, + jint id) { + jbyte *b1_native = env->GetByteArrayElements(b1, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + jbyte *b2_native = env->GetByteArrayElements(b2, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + jint b1_length = env->GetArrayLength(b1); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + jint b2_length = env->GetArrayLength(b2); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1_native, b2_native, + b1_length, b2_length, result); + env->ReleaseByteArrayElements(b1, b1_native, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + env->ReleaseByteArrayElements(b2, b2_native, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceMemcmp( + JNIEnv *env, jclass cls, jbyteArray b1, jbyteArray b2, jint result, + jint id) { + auto *b1_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(b1, nullptr)); + auto *b2_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(b2, nullptr)); + jint b1_length = env->GetArrayLength(b1); + jint b2_length = env->GetArrayLength(b2); + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1_native, b2_native, + b1_length, b2_length, result); + env->ReleasePrimitiveArrayCritical(b1, b1_native, JNI_ABORT); + env->ReleasePrimitiveArrayCritical(b2, b2_native, JNI_ABORT); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceMemcmp( + JNIEnv *env, jclass cls, jbyteArray b1, jbyteArray b2, jint result, + jint id) { + Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceMemcmp( + env, cls, b1, b2, result, id); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceMemcmp( + jint b1_length, jbyte *b1, jint b2_length, jbyte *b2, jint result, + jint id) { + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1, b2, b1_length, b2_length, + result); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceStrstr( + JNIEnv *env, jclass cls, jstring s1, jstring s2, jint id) { + const char *s1_native = env->GetStringUTFChars(s1, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + const char *s2_native = env->GetStringUTFChars(s2, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + // libFuzzer currently ignores the result, which allows us to simply pass a + // valid but arbitrary pointer here instead of performing an actual strstr + // operation. + __sanitizer_weak_hook_strstr(idToPc(id), s1_native, s2_native, s1_native); + env->ReleaseStringUTFChars(s1, s1_native); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + env->ReleaseStringUTFChars(s2, s2_native); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceStrstr( + JNIEnv *env, jclass cls, jstring s1, jstring s2, jint id) { + const char *s2_native = env->GetStringUTFChars(s2, nullptr); + __sanitizer_weak_hook_strstr(idToPc(id), nullptr, s2_native, s2_native); + env->ReleaseStringUTFChars(s2, s2_native); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceStrstrInternal( + JNIEnv *env, jclass cls, jbyteArray needle, jint id) { + auto *needle_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(needle, nullptr)); + jint needle_length = env->GetArrayLength(needle); + __sanitizer_weak_hook_memmem(idToPc(id), nullptr, 0, needle_native, + needle_length, nullptr); + env->ReleasePrimitiveArrayCritical(needle, needle_native, JNI_ABORT); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceStrstrInternal( + JNIEnv *env, jclass cls, jbyteArray needle, jint id) { + Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceStrstrInternal( + env, cls, needle, id); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceStrstrInternal( + jint needle_length, jbyte *needle, jint id) { + __sanitizer_weak_hook_memmem(idToPc(id), nullptr, 0, needle, needle_length, + nullptr); +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt index 33d02263..f9b026f1 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt @@ -16,38 +16,35 @@ package com.code_intelligence.jazzer.agent +import com.code_intelligence.jazzer.driver.Opt import com.code_intelligence.jazzer.instrumentor.CoverageRecorder +import com.code_intelligence.jazzer.instrumentor.Hooks import com.code_intelligence.jazzer.instrumentor.InstrumentationType -import com.code_intelligence.jazzer.instrumentor.loadHooks -import com.code_intelligence.jazzer.runtime.ManifestUtils +import com.code_intelligence.jazzer.runtime.NativeLibHooks +import com.code_intelligence.jazzer.runtime.TraceCmpHooks +import com.code_intelligence.jazzer.runtime.TraceDivHooks +import com.code_intelligence.jazzer.runtime.TraceIndirHooks import com.code_intelligence.jazzer.utils.ClassNameGlobber +import com.code_intelligence.jazzer.utils.ManifestUtils import java.io.File import java.lang.instrument.Instrumentation +import java.net.URI import java.nio.file.Paths import java.util.jar.JarFile import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.exists import kotlin.io.path.isDirectory -val KNOWN_ARGUMENTS = listOf( - "instrumentation_includes", - "instrumentation_excludes", - "custom_hook_includes", - "custom_hook_excludes", - "trace", - "custom_hooks", - "id_sync_file", - "dump_classes_dir", -) - private object AgentJarFinder { - private val agentJarPath = AgentJarFinder::class.java.protectionDomain?.codeSource?.location?.toURI() - val agentJarFile = agentJarPath?.let { JarFile(File(it)) } + val agentJarFile = jarUriForClass(AgentJarFinder::class.java)?.let { JarFile(File(it)) } } -private val argumentDelimiter = if (System.getProperty("os.name").startsWith("Windows")) ";" else ":" +fun jarUriForClass(clazz: Class<*>): URI? { + return clazz.protectionDomain?.codeSource?.location?.toURI() +} @OptIn(ExperimentalPathApi::class) +@Suppress("UNUSED_PARAMETER") fun premain(agentArgs: String?, instrumentation: Instrumentation) { // Add the agent jar (i.e., the jar out of which we are currently executing) to the search path of the bootstrap // class loader to ensure that instrumented classes can find the CoverageMap class regardless of which ClassLoader @@ -57,37 +54,25 @@ fun premain(agentArgs: String?, instrumentation: Instrumentation) { } else { println("WARN: Failed to add agent JAR to bootstrap class loader search path") } - val argumentMap = (agentArgs ?: "") - .split(',') - .mapNotNull { - val splitArg = it.split('=', limit = 2) - when { - splitArg.size != 2 -> { - if (splitArg[0].isNotEmpty()) - println("WARN: Ignoring argument ${splitArg[0]} without value") - null - } - splitArg[0] !in KNOWN_ARGUMENTS -> { - println("WARN: Ignoring unknown argument ${splitArg[0]}") - null - } - else -> splitArg[0] to splitArg[1].split(argumentDelimiter) - } - }.toMap() - val manifestCustomHookNames = ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES).flatMap { - it.split(':') + + val manifestCustomHookNames = + ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES).flatMap { + it.split(':') + }.filter { it.isNotBlank() } + val allCustomHookNames = (manifestCustomHookNames + Opt.customHooks).toSet() + val disabledCustomHookNames = Opt.disabledHooks.toSet() + val customHookNames = allCustomHookNames - disabledCustomHookNames + val disabledCustomHooksToPrint = allCustomHookNames - customHookNames.toSet() + if (disabledCustomHooksToPrint.isNotEmpty()) { + println("INFO: Not using the following disabled hooks: ${disabledCustomHooksToPrint.joinToString(", ")}") } - val customHookNames = manifestCustomHookNames + (argumentMap["custom_hooks"] ?: emptyList()) - val classNameGlobber = ClassNameGlobber( - argumentMap["instrumentation_includes"] ?: emptyList(), - (argumentMap["instrumentation_excludes"] ?: emptyList()) + customHookNames - ) + + val classNameGlobber = ClassNameGlobber(Opt.instrumentationIncludes, Opt.instrumentationExcludes + customHookNames) CoverageRecorder.classNameGlobber = classNameGlobber - val dependencyClassNameGlobber = ClassNameGlobber( - argumentMap["custom_hook_includes"] ?: emptyList(), - (argumentMap["custom_hook_excludes"] ?: emptyList()) + customHookNames - ) - val instrumentationTypes = (argumentMap["trace"] ?: listOf("all")).flatMap { + val customHookClassNameGlobber = ClassNameGlobber(Opt.customHookIncludes, Opt.customHookExcludes + customHookNames) + // FIXME: Setting trace to the empty string explicitly results in all rather than no trace types + // being applied - this is unintuitive. + val instrumentationTypes = (Opt.trace.takeIf { it.isNotEmpty() } ?: listOf("all")).flatMap { when (it) { "cmp" -> setOf(InstrumentationType.CMP) "cov" -> setOf(InstrumentationType.COV) @@ -106,13 +91,13 @@ fun premain(agentArgs: String?, instrumentation: Instrumentation) { } } }.toSet() - val idSyncFile = argumentMap["id_sync_file"]?.let { - Paths.get(it.single()).also { path -> + val idSyncFile = Opt.idSyncFile.takeUnless { it.isEmpty() }?.let { + Paths.get(it).also { path -> println("INFO: Synchronizing coverage IDs in ${path.toAbsolutePath()}") } } - val dumpClassesDir = argumentMap["dump_classes_dir"]?.let { - Paths.get(it.single()).toAbsolutePath().also { path -> + val dumpClassesDir = Opt.dumpClassesDir.takeUnless { it.isEmpty() }?.let { + Paths.get(it).toAbsolutePath().also { path -> if (path.exists() && path.isDirectory()) { println("INFO: Dumping instrumented classes into $path") } else { @@ -120,41 +105,67 @@ fun premain(agentArgs: String?, instrumentation: Instrumentation) { } } } + val includedHookNames = instrumentationTypes + .mapNotNull { type -> + when (type) { + InstrumentationType.CMP -> TraceCmpHooks::class.java.name + InstrumentationType.DIV -> TraceDivHooks::class.java.name + InstrumentationType.INDIR -> TraceIndirHooks::class.java.name + InstrumentationType.NATIVE -> NativeLibHooks::class.java.name + else -> null + } + } + val coverageIdSynchronizer = if (idSyncFile != null) + FileSyncCoverageIdStrategy(idSyncFile) + else + MemSyncCoverageIdStrategy() + + val (includedHooks, customHooks) = Hooks.loadHooks(includedHookNames.toSet(), customHookNames.toSet()) + // If we don't append the JARs containing the custom hooks to the bootstrap class loader, + // third-party hooks not contained in the agent JAR will not be able to instrument Java standard + // library classes. These classes are loaded by the bootstrap / system class loader and would + // not be considered when resolving references to hook methods, leading to NoClassDefFoundError + // being thrown. + customHooks.hookClasses + .mapNotNull { jarUriForClass(it) } + .toSet() + .map { JarFile(File(it)) } + .forEach { instrumentation.appendToBootstrapClassLoaderSearch(it) } + val runtimeInstrumentor = RuntimeInstrumentor( instrumentation, classNameGlobber, - dependencyClassNameGlobber, + customHookClassNameGlobber, instrumentationTypes, - idSyncFile, + includedHooks.hooks, + customHooks.hooks, + customHooks.additionalHookClassNameGlobber, + coverageIdSynchronizer, dumpClassesDir, ) - instrumentation.apply { - addTransformer(runtimeInstrumentor) - } - val relevantClassesLoadedBeforeCustomHooks = instrumentation.allLoadedClasses - .map { it.name } - .filter { classNameGlobber.includes(it) || dependencyClassNameGlobber.includes(it) } - .toSet() - val customHooks = customHookNames.toSet().flatMap { hookClassName -> - try { - loadHooks(Class.forName(hookClassName)).also { - println("INFO: Loaded ${it.size} hooks from $hookClassName") - } - } catch (_: ClassNotFoundException) { - println("WARN: Failed to load hooks from $hookClassName") - emptySet() + // These classes are e.g. dependencies of the RuntimeInstrumentor or hooks and thus were loaded + // before the instrumentor was ready. Since we haven't enabled it yet, they can safely be + // "retransformed": They haven't been transformed yet. + val classesToRetransform = instrumentation.allLoadedClasses + .filter { + classNameGlobber.includes(it.name) || + customHookClassNameGlobber.includes(it.name) || + customHooks.additionalHookClassNameGlobber.includes(it.name) } - } - val relevantClassesLoadedAfterCustomHooks = instrumentation.allLoadedClasses - .map { it.name } - .filter { classNameGlobber.includes(it) || dependencyClassNameGlobber.includes(it) } - .toSet() - val nonHookClassesLoadedByHooks = relevantClassesLoadedAfterCustomHooks - relevantClassesLoadedBeforeCustomHooks - if (nonHookClassesLoadedByHooks.isNotEmpty()) { - println("WARN: Hooks were not applied to the following classes as they are dependencies of hooks:") - println("WARN: ${nonHookClassesLoadedByHooks.joinToString()}") - } + .filter { + instrumentation.isModifiableClass(it) + } + .toTypedArray() + + instrumentation.addTransformer(runtimeInstrumentor, true) - runtimeInstrumentor.registerCustomHooks(customHooks) + if (classesToRetransform.isNotEmpty()) { + if (instrumentation.isRetransformClassesSupported) { + instrumentation.retransformClasses(*classesToRetransform) + } else { + println("WARN: Instrumentation was not applied to the following classes as they are dependencies of hooks:") + println("WARN: ${classesToRetransform.joinToString()}") + } + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel index 2d5eec5c..db6ae264 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel @@ -11,5 +11,6 @@ kt_jvm_library( deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:opt", ], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt index fd2a1e7c..5d1d28e3 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt @@ -14,7 +14,8 @@ package com.code_intelligence.jazzer.agent -import java.nio.ByteBuffer +import com.code_intelligence.jazzer.utils.append +import com.code_intelligence.jazzer.utils.readFully import java.nio.channels.FileChannel import java.nio.channels.FileLock import java.nio.file.Path @@ -24,59 +25,42 @@ import java.util.UUID /** * Indicates a fatal failure to generate synchronized coverage IDs. */ -internal class CoverageIdException(cause: Throwable? = null) : +class CoverageIdException(cause: Throwable? = null) : RuntimeException("Failed to synchronize coverage IDs", cause) +/** + * [CoverageIdStrategy] provides an abstraction to switch between context specific coverage ID generation. + * + * Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp + * instructions, in that they should be consecutive, collision-free, and lie in a known, small range. + * This precludes us from generating them simply as hashes of class names. + */ interface CoverageIdStrategy { - /** - * Obtain the first coverage ID to be used for the class [className]. - * The caller *must* also call [commitIdCount] once it has instrumented that class, even if instrumentation fails. - */ - @Throws(CoverageIdException::class) - fun obtainFirstId(className: String): Int /** - * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId]. - * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0. + * [withIdForClass] provides the initial coverage ID of the given [className] as parameter to the + * [block] to execute. [block] has to return the number of additionally used IDs. */ @Throws(CoverageIdException::class) - fun commitIdCount(idCount: Int) + fun withIdForClass(className: String, block: (Int) -> Int) } /** - * An unsynchronized strategy for coverage ID generation that simply increments a global counter. + * A memory synced strategy for coverage ID generation. + * + * This strategy uses a synchronized block to guard access to a global edge ID counter. + * Even though concurrent fuzzing is not fully supported this strategy enables consistent coverage + * IDs in case of concurrent class loading. + * + * It only prevents races within one VM instance. */ -internal class TrivialCoverageIdStrategy : CoverageIdStrategy { +class MemSyncCoverageIdStrategy : CoverageIdStrategy { private var nextEdgeId = 0 - override fun obtainFirstId(className: String) = nextEdgeId - - override fun commitIdCount(idCount: Int) { - nextEdgeId += idCount - } -} - -/** - * Reads the [FileChannel] to the end as a UTF-8 string. - */ -private fun FileChannel.readFully(): String { - check(size() <= Int.MAX_VALUE) - val buffer = ByteBuffer.allocate(size().toInt()) - while (buffer.hasRemaining()) { - when (read(buffer)) { - 0 -> throw IllegalStateException("No bytes read") - -1 -> break - } + @Synchronized + override fun withIdForClass(className: String, block: (Int) -> Int) { + nextEdgeId += block(nextEdgeId) } - return String(buffer.array()) -} - -/** - * Appends [string] to the end of the [FileChannel]. - */ -private fun FileChannel.append(string: String) { - position(size()) - write(ByteBuffer.wrap(string.toByteArray())) } /** @@ -84,19 +68,30 @@ private fun FileChannel.append(string: String) { * specified [idSyncFile]. * This class takes care of synchronizing the access to the file between multiple processes as long as the general * contract of [CoverageIdStrategy] is followed. - * - * Rationale: Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp - * instructions, in that they should be consecutive, collision-free, and lie in a known, small range. This precludes us - * from generating them simply as hashes of class names and explains why go through the arduous process of synchronizing - * them across multiple agents. */ -internal class SynchronizedCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy { - val uuid: UUID = UUID.randomUUID() - var idFileLock: FileLock? = null +class FileSyncCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy { + private val uuid: UUID = UUID.randomUUID() + private var idFileLock: FileLock? = null + + private var cachedFirstId: Int? = null + private var cachedClassName: String? = null + private var cachedIdCount: Int? = null - var cachedFirstId: Int? = null - var cachedClassName: String? = null - var cachedIdCount: Int? = null + /** + * This method is synchronized to prevent concurrent access to the internal file lock which would result in + * [java.nio.channels.OverlappingFileLockException]. Furthermore, every coverage ID obtained by [obtainFirstId] + * is always committed back again to the sync file by [commitIdCount]. + */ + @Synchronized + override fun withIdForClass(className: String, block: (Int) -> Int) { + var actualNumEdgeIds = 0 + try { + val firstId = obtainFirstId(className) + actualNumEdgeIds = block(firstId) + } finally { + commitIdCount(actualNumEdgeIds) + } + } /** * Obtains a coverage ID for [className] such that all cooperating agent processes will obtain the same ID. @@ -108,7 +103,7 @@ internal class SynchronizedCoverageIdStrategy(private val idSyncFile: Path) : Co * In this case, the lock on the file is returned immediately and the extracted first coverage ID is returned to * the caller. The caller is still expected to call [commitIdCount] so that desynchronization can be detected. */ - override fun obtainFirstId(className: String): Int { + private fun obtainFirstId(className: String): Int { try { check(idFileLock == null) { "Already holding a lock on the ID file" } val localIdFile = FileChannel.open( @@ -170,7 +165,11 @@ internal class SynchronizedCoverageIdStrategy(private val idSyncFile: Path) : Co } } - override fun commitIdCount(idCount: Int) { + /** + * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId]. + * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0. + */ + private fun commitIdCount(idCount: Int) { val localIdFileLock = idFileLock try { check(cachedClassName != null) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt index e2283aa2..fe2efd54 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt @@ -18,11 +18,6 @@ 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.loadHooks -import com.code_intelligence.jazzer.runtime.NativeLibHooks -import com.code_intelligence.jazzer.runtime.TraceCmpHooks -import com.code_intelligence.jazzer.runtime.TraceDivHooks -import com.code_intelligence.jazzer.runtime.TraceIndirHooks import com.code_intelligence.jazzer.utils.ClassNameGlobber import java.lang.instrument.ClassFileTransformer import java.lang.instrument.Instrumentation @@ -32,37 +27,25 @@ import kotlin.math.roundToInt import kotlin.system.exitProcess import kotlin.time.measureTimedValue -internal class RuntimeInstrumentor( +class RuntimeInstrumentor( private val instrumentation: Instrumentation, - private val classesToInstrument: ClassNameGlobber, - private val dependencyClassesToInstrument: ClassNameGlobber, + private val classesToFullyInstrument: ClassNameGlobber, + private val classesToHookInstrument: ClassNameGlobber, private val instrumentationTypes: Set<InstrumentationType>, - idSyncFile: Path?, + private val includedHooks: List<Hook>, + private val customHooks: List<Hook>, + // Dedicated name globber for additional classes to hook stated in hook annotations is needed due to + // existing include and exclude pattern of classesToHookInstrument. All classes are included in hook + // instrumentation except the ones from default excludes, like JDK and Kotlin classes. But additional + // classes to hook, based on annotations, are allowed to reference normally ignored ones, like JDK + // and Kotlin internals. + // FIXME: Adding an additional class to hook will apply _all_ hooks to it and not only the one it's + // defined in. At some point we might want to track the list of classes per custom hook rather than globally. + private val additionalClassesToHookInstrument: ClassNameGlobber, + private val coverageIdSynchronizer: CoverageIdStrategy, private val dumpClassesDir: Path?, ) : ClassFileTransformer { - private val coverageIdSynchronizer = if (idSyncFile != null) - SynchronizedCoverageIdStrategy(idSyncFile) - else - TrivialCoverageIdStrategy() - - private val includedHooks = instrumentationTypes - .mapNotNull { type -> - when (type) { - InstrumentationType.CMP -> TraceCmpHooks::class.java - InstrumentationType.DIV -> TraceDivHooks::class.java - InstrumentationType.INDIR -> TraceIndirHooks::class.java - InstrumentationType.NATIVE -> NativeLibHooks::class.java - else -> null - } - } - .flatMap { loadHooks(it) } - private val customHooks = emptyList<Hook>().toMutableList() - - fun registerCustomHooks(hooks: List<Hook>) { - customHooks.addAll(hooks) - } - @OptIn(kotlin.time.ExperimentalTime::class) override fun transform( loader: ClassLoader?, @@ -86,15 +69,20 @@ internal class RuntimeInstrumentor( }.also { instrumentedByteCode -> // Only dump classes that were instrumented. if (instrumentedByteCode != null && dumpClassesDir != null) { - val relativePath = "$internalClassName.class" - val absolutePath = dumpClassesDir.resolve(relativePath) - val dumpFile = absolutePath.toFile() - dumpFile.parentFile.mkdirs() - dumpFile.writeBytes(instrumentedByteCode) + dumpToClassFile(internalClassName, instrumentedByteCode) + dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".original") } } } + private fun dumpToClassFile(internalClassName: String, bytecode: ByteArray, basenameSuffix: String = "") { + val relativePath = "$internalClassName$basenameSuffix.class" + val absolutePath = dumpClassesDir!!.resolve(relativePath) + val dumpFile = absolutePath.toFile() + dumpFile.parentFile.mkdirs() + dumpFile.writeBytes(bytecode) + } + override fun transform( module: Module?, loader: ClassLoader?, @@ -103,33 +91,42 @@ internal class RuntimeInstrumentor( protectionDomain: ProtectionDomain?, classfileBuffer: ByteArray ): ByteArray? { - if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) { - // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the - // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the - // injected bytecode might throw NoClassDefFoundError. - // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html - if (!instrumentation.isModifiableModule(module)) { - val prettyClassName = internalClassName.replace('/', '.') - println("WARN: Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping") - return null + return try { + if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) { + // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the + // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the + // injected bytecode might throw NoClassDefFoundError. + // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html + if (!instrumentation.isModifiableModule(module)) { + val prettyClassName = internalClassName.replace('/', '.') + println("WARN: Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping") + return null + } + instrumentation.redefineModule( + module, + /* extraReads */ setOf(RuntimeInstrumentor::class.java.module), + emptyMap(), + emptyMap(), + emptySet(), + emptyMap() + ) } - instrumentation.redefineModule( - module, - /* extraReads */ setOf(RuntimeInstrumentor::class.java.module), - emptyMap(), - emptyMap(), - emptySet(), - emptyMap() - ) + transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer) + } catch (t: Throwable) { + // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation + // failures. The docs advise to use a top-level try-catch. + // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html + t.printStackTrace() + throw t } - return transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer) } @OptIn(kotlin.time.ExperimentalTime::class) fun transformInternal(internalClassName: String, classfileBuffer: ByteArray): ByteArray? { val fullInstrumentation = when { - classesToInstrument.includes(internalClassName) -> true - dependencyClassesToInstrument.includes(internalClassName) -> false + classesToFullyInstrument.includes(internalClassName) -> true + classesToHookInstrument.includes(internalClassName) -> false + additionalClassesToHookInstrument.includes(internalClassName) -> false else -> return null } val prettyClassName = internalClassName.replace('/', '.') @@ -165,14 +162,16 @@ internal class RuntimeInstrumentor( // trigger the GEP callbacks for ByteBuffer. traceDataFlow(instrumentationTypes) hooks(includedHooks + customHooks) - val firstId = coverageIdSynchronizer.obtainFirstId(internalClassName) - var actualNumEdgeIds = 0 - try { - actualNumEdgeIds = coverage(firstId) - } finally { - coverageIdSynchronizer.commitIdCount(actualNumEdgeIds) + coverageIdSynchronizer.withIdForClass(internalClassName) { firstId -> + coverage(firstId).also { actualNumEdgeIds -> + CoverageRecorder.recordInstrumentedClass( + internalClassName, + bytecode, + firstId, + actualNumEdgeIds + ) + } } - CoverageRecorder.recordInstrumentedClass(internalClassName, bytecode, firstId, firstId + actualNumEdgeIds) } else { hooks(customHooks) } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel index e573e757..b26bb846 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -23,6 +23,7 @@ java_library( "Jazzer.java", "MethodHook.java", "MethodHooks.java", + "//agent/src/main/java/jaz", ], visibility = ["//visibility:public"], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java index 4402a7f3..fbde853b 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java @@ -17,7 +17,7 @@ package com.code_intelligence.jazzer.api; /** * Thrown to indicate that a fuzz target has detected a critical severity security issue rather than * a normal bug. - * + * <p> * There is only a semantical but no functional difference between throwing exceptions of this type * or any other. However, automated fuzzing platforms can use the extra information to handle the * detected issues appropriately. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java index 4d323e56..05837b0e 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java @@ -17,7 +17,7 @@ package com.code_intelligence.jazzer.api; /** * Thrown to indicate that a fuzz target has detected a high severity security issue rather than a * normal bug. - * + * <p> * There is only a semantical but no functional difference between throwing exceptions of this type * or any other. However, automated fuzzing platforms can use the extra information to handle the * detected issues appropriately. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java index f0de4ce7..be7c8c8f 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java @@ -17,7 +17,7 @@ package com.code_intelligence.jazzer.api; /** * Thrown to indicate that a fuzz target has detected a medium severity security issue rather than a * normal bug. - * + * <p> * There is only a semantical but no functional difference between throwing exceptions of this type * or any other. However, automated fuzzing platforms can use the extra information to handle the * detected issues appropriately. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java b/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java index 1c564a78..8ed4337f 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java @@ -17,6 +17,7 @@ package com.code_intelligence.jazzer.api; /** * The type of a {@link MethodHook}. */ +// Note: The order of entries is important and is used during instrumentation. public enum HookType { BEFORE, REPLACE, diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java index e45f7600..97adf578 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java @@ -18,32 +18,69 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.InvocationTargetException; +import java.security.SecureRandom; /** * Helper class with static methods that interact with Jazzer at runtime. */ final public class Jazzer { - private static Class<?> jazzerInternal = null; - - private static MethodHandle traceStrcmp = null; - private static MethodHandle traceStrstr = null; - private static MethodHandle traceMemcmp = null; - - private static MethodHandle consume = null; - private static MethodHandle autofuzzFunction1 = null; - private static MethodHandle autofuzzFunction2 = null; - private static MethodHandle autofuzzFunction3 = null; - private static MethodHandle autofuzzFunction4 = null; - private static MethodHandle autofuzzFunction5 = null; - private static MethodHandle autofuzzConsumer1 = null; - private static MethodHandle autofuzzConsumer2 = null; - private static MethodHandle autofuzzConsumer3 = null; - private static MethodHandle autofuzzConsumer4 = null; - private static MethodHandle autofuzzConsumer5 = null; + /** + * A 32-bit random number that hooks can use to make pseudo-random choices + * between multiple possible mutations they could guide the fuzzer towards. + * Hooks <b>must not</b> base the decision whether or not to report a finding + * on this number as this will make findings non-reproducible. + * <p> + * This is the same number that libFuzzer uses as a seed internally, which + * makes it possible to deterministically reproduce a previous fuzzing run by + * supplying the seed value printed by libFuzzer as the value of the + * {@code -seed}. + */ + public static final int SEED = getLibFuzzerSeed(); + + private static final Class<?> JAZZER_INTERNAL; + + private static final MethodHandle ON_FUZZ_TARGET_READY; + + private static final MethodHandle TRACE_STRCMP; + private static final MethodHandle TRACE_STRSTR; + private static final MethodHandle TRACE_MEMCMP; + private static final MethodHandle TRACE_PC_INDIR; + + private static final MethodHandle CONSUME; + private static final MethodHandle AUTOFUZZ_FUNCTION_1; + private static final MethodHandle AUTOFUZZ_FUNCTION_2; + private static final MethodHandle AUTOFUZZ_FUNCTION_3; + private static final MethodHandle AUTOFUZZ_FUNCTION_4; + private static final MethodHandle AUTOFUZZ_FUNCTION_5; + private static final MethodHandle AUTOFUZZ_CONSUMER_1; + private static final MethodHandle AUTOFUZZ_CONSUMER_2; + private static final MethodHandle AUTOFUZZ_CONSUMER_3; + private static final MethodHandle AUTOFUZZ_CONSUMER_4; + private static final MethodHandle AUTOFUZZ_CONSUMER_5; static { + Class<?> jazzerInternal = null; + MethodHandle onFuzzTargetReady = null; + MethodHandle traceStrcmp = null; + MethodHandle traceStrstr = null; + MethodHandle traceMemcmp = null; + MethodHandle tracePcIndir = null; + MethodHandle consume = null; + MethodHandle autofuzzFunction1 = null; + MethodHandle autofuzzFunction2 = null; + MethodHandle autofuzzFunction3 = null; + MethodHandle autofuzzFunction4 = null; + MethodHandle autofuzzFunction5 = null; + MethodHandle autofuzzConsumer1 = null; + MethodHandle autofuzzConsumer2 = null; + MethodHandle autofuzzConsumer3 = null; + MethodHandle autofuzzConsumer4 = null; + MethodHandle autofuzzConsumer5 = null; try { jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal"); + MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class); + onFuzzTargetReady = MethodHandles.publicLookup().findStatic( + jazzerInternal, "registerOnFuzzTargetReadyCallback", onFuzzTargetReadyType); Class<?> traceDataFlowNativeCallbacks = Class.forName("com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks"); @@ -60,6 +97,9 @@ final public class Jazzer { MethodType.methodType(void.class, byte[].class, byte[].class, int.class, int.class); traceMemcmp = MethodHandles.publicLookup().findStatic( traceDataFlowNativeCallbacks, "traceMemcmp", traceMemcmpType); + MethodType tracePcIndirType = MethodType.methodType(void.class, int.class, int.class); + tracePcIndir = MethodHandles.publicLookup().findStatic( + traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType); Class<?> metaClass = Class.forName("com.code_intelligence.jazzer.autofuzz.Meta"); MethodType consumeType = @@ -96,6 +136,23 @@ final public class Jazzer { e.printStackTrace(); System.exit(1); } + JAZZER_INTERNAL = jazzerInternal; + ON_FUZZ_TARGET_READY = onFuzzTargetReady; + TRACE_STRCMP = traceStrcmp; + TRACE_STRSTR = traceStrstr; + TRACE_MEMCMP = traceMemcmp; + TRACE_PC_INDIR = tracePcIndir; + CONSUME = consume; + AUTOFUZZ_FUNCTION_1 = autofuzzFunction1; + AUTOFUZZ_FUNCTION_2 = autofuzzFunction2; + AUTOFUZZ_FUNCTION_3 = autofuzzFunction3; + AUTOFUZZ_FUNCTION_4 = autofuzzFunction4; + AUTOFUZZ_FUNCTION_5 = autofuzzFunction5; + AUTOFUZZ_CONSUMER_1 = autofuzzConsumer1; + AUTOFUZZ_CONSUMER_2 = autofuzzConsumer2; + AUTOFUZZ_CONSUMER_3 = autofuzzConsumer3; + AUTOFUZZ_CONSUMER_4 = autofuzzConsumer4; + AUTOFUZZ_CONSUMER_5 = autofuzzConsumer5; } private Jazzer() {} @@ -103,7 +160,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -120,7 +177,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) { try { - return (R) autofuzzFunction1.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_1.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -133,7 +190,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -150,7 +207,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) { try { - return (R) autofuzzFunction2.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_2.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -163,7 +220,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -180,7 +237,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) { try { - return (R) autofuzzFunction3.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_3.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -193,7 +250,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -211,7 +268,7 @@ final public class Jazzer { public static <T1, T2, T3, T4, R> R autofuzz( FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) { try { - return (R) autofuzzFunction4.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_4.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -224,7 +281,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -242,7 +299,7 @@ final public class Jazzer { public static <T1, T2, T3, T4, T5, R> R autofuzz( FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) { try { - return (R) autofuzzFunction5.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_5.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -255,7 +312,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -269,7 +326,7 @@ final public class Jazzer { */ public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) { try { - autofuzzConsumer1.invoke(data, func); + AUTOFUZZ_CONSUMER_1.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -280,7 +337,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -294,7 +351,7 @@ final public class Jazzer { */ public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) { try { - autofuzzConsumer2.invoke(data, func); + AUTOFUZZ_CONSUMER_2.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -305,7 +362,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -319,7 +376,7 @@ final public class Jazzer { */ public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) { try { - autofuzzConsumer3.invoke(data, func); + AUTOFUZZ_CONSUMER_3.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -330,7 +387,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -345,7 +402,7 @@ final public class Jazzer { public static <T1, T2, T3, T4> void autofuzz( FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) { try { - autofuzzConsumer4.invoke(data, func); + AUTOFUZZ_CONSUMER_4.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -356,7 +413,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -371,7 +428,7 @@ final public class Jazzer { public static <T1, T2, T3, T4, T5> void autofuzz( FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) { try { - autofuzzConsumer5.invoke(data, func); + AUTOFUZZ_CONSUMER_5.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -382,7 +439,7 @@ final public class Jazzer { /** * Attempts to construct an instance of {@code type} from the fuzzer input using only public * methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to return meaningful values for * a variety of reasons. * @@ -394,7 +451,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T> T consume(FuzzedDataProvider data, Class<T> type) { try { - return (T) consume.invokeExact(data, type); + return (T) CONSUME.invokeExact(data, type); } catch (AutofuzzConstructionException ignored) { return null; } catch (Throwable t) { @@ -407,7 +464,7 @@ final public class Jazzer { /** * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code * target}. - * + * <p> * If the relation between the raw fuzzer input and the value of {@code current} is relatively * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to * achieve equality. @@ -417,8 +474,11 @@ final public class Jazzer { * @param id a (probabilistically) unique identifier for this particular compare hint */ public static void guideTowardsEquality(String current, String target, int id) { + if (TRACE_STRCMP == null) { + return; + } try { - traceStrcmp.invokeExact(current, target, 1, id); + TRACE_STRCMP.invokeExact(current, target, 1, id); } catch (Throwable e) { e.printStackTrace(); } @@ -427,7 +487,7 @@ final public class Jazzer { /** * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code * target}. - * + * <p> * If the relation between the raw fuzzer input and the value of {@code current} is relatively * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to * achieve equality. @@ -437,8 +497,11 @@ final public class Jazzer { * @param id a (probabilistically) unique identifier for this particular compare hint */ public static void guideTowardsEquality(byte[] current, byte[] target, int id) { + if (TRACE_MEMCMP == null) { + return; + } try { - traceMemcmp.invokeExact(current, target, 1, id); + TRACE_MEMCMP.invokeExact(current, target, 1, id); } catch (Throwable e) { e.printStackTrace(); } @@ -447,7 +510,7 @@ final public class Jazzer { /** * Instructs the fuzzer to guide its mutations towards making {@code haystack} contain {@code * needle} as a substring. - * + * <p> * If the relation between the raw fuzzer input and the value of {@code haystack} is relatively * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to * satisfy the substring check. @@ -458,28 +521,81 @@ final public class Jazzer { * @param id a (probabilistically) unique identifier for this particular compare hint */ public static void guideTowardsContainment(String haystack, String needle, int id) { + if (TRACE_STRSTR == null) { + return; + } try { - traceStrstr.invokeExact(haystack, needle, id); + TRACE_STRSTR.invokeExact(haystack, needle, id); } catch (Throwable e) { e.printStackTrace(); } } /** - * Make Jazzer report the provided {@link Throwable} as a finding. + * Instructs the fuzzer to attain as many possible values for the absolute value of {@code state} + * as possible. + * <p> + * Call this function from a fuzz target or a hook to help the fuzzer track partial progress + * (e.g. by passing the length of a common prefix of two lists that should become equal) or + * explore different values of state that is not directly related to code coverage (see the + * MazeFuzzer example). + * <p> + * <b>Note:</b> This hint only takes effect if the fuzzer is run with the argument + * {@code -use_value_profile=1}. * + * @param state a numeric encoding of a state that should be varied by the fuzzer + * @param id a (probabilistically) unique identifier for this particular state hint + */ + public static void exploreState(byte state, int id) { + if (TRACE_PC_INDIR == null) { + return; + } + // We only use the lower 7 bits of state, which allows for 128 different state values tracked + // per id. The particular amount of 7 bits of state is also used in libFuzzer's + // TracePC::HandleCmp: + // https://github.com/llvm/llvm-project/blob/c12d49c4e286fa108d4d69f1c6d2b8d691993ffd/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L390 + // This value should be large enough for most use cases (e.g. tracking the length of a prefix in + // a comparison) while being small enough that the bitmap isn't filled up too quickly + // (65536 bits/ 128 bits per id = 512 ids). + + // We use tracePcIndir as a way to set a bit in libFuzzer's value profile bitmap. In + // TracePC::HandleCallerCallee, which is what this function ultimately calls through to, the + // lower 12 bits of each argument are combined into a 24-bit index into the bitmap, which is + // then reduced modulo a 16-bit prime. To keep the modulo bias small, we should fill as many + // of the relevant bits as possible. However, there are the following restrictions: + // 1. Since we use the return address trampoline to set the caller address indirectly, its + // upper 3 bits are fixed, which leaves a total of 21 variable bits on x86_64. + // 2. On arm64 macOS, where every instruction is aligned to 4 bytes, the lower 2 bits of the + // caller address will always be zero, further reducing the number of variable bits in the + // caller parameter to 7. + // https://github.com/llvm/llvm-project/blob/c12d49c4e286fa108d4d69f1c6d2b8d691993ffd/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L121 + // Even taking these restrictions into consideration, we pass state in the lowest bits of the + // caller address, which is used to form the lowest bits of the bitmap index. This should result + // in the best caching behavior as state is expected to change quickly in consecutive runs and + // in this way all its bitmap entries would be located close to each other in memory. + int lowerBits = (state & 0x7f) | (id << 7); + int upperBits = id >>> 5; + try { + TRACE_PC_INDIR.invokeExact(upperBits, lowerBits); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /** + * Make Jazzer report the provided {@link Throwable} as a finding. + * <p> * <b>Note:</b> This method must only be called from a method hook. In a * fuzz target, simply throw an exception to trigger a finding. * @param finding the finding that Jazzer should report */ public static void reportFindingFromHook(Throwable finding) { try { - jazzerInternal.getMethod("reportFindingFromHook", Throwable.class).invoke(null, finding); + JAZZER_INTERNAL.getMethod("reportFindingFromHook", Throwable.class).invoke(null, finding); } catch (NullPointerException | IllegalAccessException | NoSuchMethodException e) { - // We can only reach this point if the runtime is not in the classpath, but it must be if - // hooks work and this function should only be called from them. - System.err.println("ERROR: Jazzer.reportFindingFromHook must be called from a method hook"); - System.exit(1); + // We can only reach this point if the runtime is not on the classpath, e.g. in case of a + // reproducer. Just throw the finding. + rethrowUnchecked(finding); } catch (InvocationTargetException e) { // reportFindingFromHook throws a HardToCatchThrowable, which will bubble up wrapped in an // InvocationTargetException that should not be stopped here. @@ -491,6 +607,34 @@ final public class Jazzer { } } + /** + * Register a callback to be executed right before the fuzz target is executed for the first time. + * <p> + * This can be used to disable hooks until after Jazzer has been fully initializing, e.g. to + * prevent Jazzer internals from triggering hooks on Java standard library classes. + * + * @param callback the callback to execute + */ + public static void onFuzzTargetReady(Runnable callback) { + try { + ON_FUZZ_TARGET_READY.invokeExact(callback); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private static int getLibFuzzerSeed() { + // The Jazzer driver sets this property based on the value of libFuzzer's -seed command-line + // option, which allows for fully reproducible fuzzing runs if set. If not running in the + // context of the driver, fall back to a random number instead. + String rawSeed = System.getProperty("jazzer.seed"); + if (rawSeed == null) { + return new SecureRandom().nextInt(); + } + // If jazzer.seed is set, we expect it to be a valid integer. + return Integer.parseUnsignedInt(rawSeed); + } + // Rethrows a (possibly checked) exception while avoiding a throws declaration. @SuppressWarnings("unchecked") private static <T extends Throwable> void rethrowUnchecked(Throwable t) throws T { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java index 0d17a4a0..3a1c5f39 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java @@ -23,11 +23,12 @@ import java.lang.annotation.Target; import java.lang.invoke.MethodType; /** - * Registers this method as a hook that should run after the method - * specified by the annotation parameters has returned. + * Registers the annotated method as a hook that should run before, instead or + * after the method specified by the annotation parameters. * <p> - * This method will be called after every call to the target method and has - * access to its return value. The target method is specified by + * Depending on {@link #type()} this method will be called after, instead or + * before every call to the target method and has + * access to its parameters and return value. The target method is specified by * {@link #targetClassName()} and {@link #targetMethod()}. In case of an * overloaded method, {@link #targetMethodDescriptor()} can be used to restrict * the application of the hook to a particular overload. @@ -87,7 +88,7 @@ import java.lang.invoke.MethodType; * <p> * Return value: the value that should take the role of the value the target * method would have returned - * + * <p> * <dt><span class="strong">{@link HookType#AFTER}</span> * <dd> * <pre>{@code @@ -114,6 +115,13 @@ import java.lang.invoke.MethodType; * will be wrapped into their corresponding wrapper type (e.g. {@link Boolean}). * If the original method has return type {@code void}, this value will be * {@code null}. + * <p> + * Multiple {@link HookType#BEFORE} and {@link HookType#AFTER} hooks are + * allowed to reference the same target method. Exclusively one + * {@link HookType#REPLACE} hook may reference a target method, no other types + * allowed. Attention must be paid to not guide the Fuzzer in different + * directions via {@link Jazzer}'s {@code guideTowardsXY} methods in the + * different hooks. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -142,6 +150,11 @@ public @interface MethodHook { * The name of the class that contains the method that should be hooked, * as returned by {@link Class#getName()}. * <p> + * If an interface or abstract class is specified, also calls to all + * implementations and subclasses available on the classpath during startup + * are hooked, respectively. Interfaces and subclasses are not taken into + * account for concrete classes. + * <p> * Examples: * <p><ul> * <li>{@link String}: {@code "java.lang.String"} @@ -180,4 +193,15 @@ public @interface MethodHook { * @return the descriptor of the method to be hooked */ String targetMethodDescriptor() default ""; + + /** + * Array of additional classes to hook. + * <p> + * Hooks are applied on call sites. This means that classes calling the one + * defined in this annotation need to be instrumented to actually execute + * the hook. This property can be used to hook normally ignored classes. + * + * @return fully qualified class names to hook + */ + String[] additionalClassesToHook() default {}; } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java index 8c344621..3b0d046b 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java @@ -20,11 +20,18 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.code_intelligence.jazzer.utils.SimpleGlobMatcher; import com.code_intelligence.jazzer.utils.Utils; import java.io.Closeable; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -32,7 +39,12 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -public class FuzzTarget { +public final class FuzzTarget { + private static final String AUTOFUZZ_REPRODUCER_TEMPLATE = "public class Crash_%s {\n" + + " public static void main(String[] args) throws Throwable {\n" + + " %s;\n" + + " }\n" + + "}"; private static final long MAX_EXECUTIONS_WITHOUT_INVOCATION = 100; private static String methodReference; @@ -40,7 +52,6 @@ public class FuzzTarget { private static Map<Executable, Class<?>[]> throwsDeclarations; private static Set<SimpleGlobMatcher> ignoredExceptionMatchers; private static long executionsSinceLastInvocation = 0; - private static AutofuzzCodegenVisitor codegenVisitor; public static void fuzzerInitialize(String[] args) { if (args.length == 0 || !args[0].contains("::")) { @@ -73,19 +84,28 @@ public class FuzzTarget { descriptor = null; } - Class<?> targetClass; - try { - // Explicitly invoking static initializers to trigger some coverage in the code. - targetClass = Class.forName(className, true, ClassLoader.getSystemClassLoader()); - } catch (ClassNotFoundException e) { - System.err.printf( - "Failed to find class %s for autofuzz, please ensure it is contained in the classpath " - + "specified with --cp and specify the full package name%n", - className); - e.printStackTrace(); - System.exit(1); - return; - } + Class<?> targetClass = null; + String targetClassName = className; + do { + try { + // Explicitly invoking static initializers to trigger some coverage in the code. + targetClass = Class.forName(targetClassName, true, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException e) { + int classSeparatorIndex = targetClassName.lastIndexOf("."); + if (classSeparatorIndex == -1) { + System.err.printf( + "Failed to find class %s for autofuzz, please ensure it is contained in the classpath " + + "specified with --cp and specify the full package name%n", + className); + e.printStackTrace(); + System.exit(1); + return; + } + StringBuilder classNameBuilder = new StringBuilder(targetClassName); + classNameBuilder.setCharAt(classSeparatorIndex, '$'); + targetClassName = classNameBuilder.toString(); + } + } while (targetClass == null); boolean isConstructor = methodName.equals("new"); if (isConstructor) { @@ -96,8 +116,13 @@ public class FuzzTarget { || Utils.getReadableDescriptor(constructor).equals(descriptor)) .toArray(Executable[] ::new); } else { + // We use getDeclaredMethods and filter for the public access modifier instead of using + // getMethods as we want to exclude methods inherited from superclasses or interfaces, which + // can lead to unexpected results when autofuzzing. If desired, these can be autofuzzed + // explicitly instead. targetExecutables = - Arrays.stream(targetClass.getMethods()) + Arrays.stream(targetClass.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) .filter(method -> method.getName().equals(methodName) && (descriptor == null @@ -179,9 +204,36 @@ public class FuzzTarget { } public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Throwable { + AutofuzzCodegenVisitor codegenVisitor = null; if (Meta.isDebug()) { codegenVisitor = new AutofuzzCodegenVisitor(); } + fuzzerTestOneInput(data, codegenVisitor); + if (codegenVisitor != null) { + System.err.println(codegenVisitor.generate()); + } + } + + public static void dumpReproducer(FuzzedDataProvider data, String reproducerPath, String sha) { + AutofuzzCodegenVisitor codegenVisitor = new AutofuzzCodegenVisitor(); + try { + fuzzerTestOneInput(data, codegenVisitor); + } catch (Throwable ignored) { + } + String javaSource = String.format(AUTOFUZZ_REPRODUCER_TEMPLATE, sha, codegenVisitor.generate()); + Path javaPath = Paths.get(reproducerPath, String.format("Crash_%s.java", sha)); + try { + Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + } catch (IOException e) { + System.err.printf("ERROR: Failed to write Java reproducer to %s%n", javaPath); + e.printStackTrace(); + } + System.out.printf( + "reproducer_path='%s'; Java reproducer written to %s%n", reproducerPath, javaPath); + } + + private static void fuzzerTestOneInput( + FuzzedDataProvider data, AutofuzzCodegenVisitor codegenVisitor) throws Throwable { Executable targetExecutable; if (FuzzTarget.targetExecutables.length == 1) { targetExecutable = FuzzTarget.targetExecutables[0]; @@ -196,9 +248,6 @@ public class FuzzTarget { returnValue = Meta.autofuzz(data, (Constructor<?>) targetExecutable, codegenVisitor); } executionsSinceLastInvocation = 0; - if (codegenVisitor != null) { - System.err.println(codegenVisitor.generate()); - } } catch (AutofuzzConstructionException e) { if (Meta.isDebug()) { e.printStackTrace(); @@ -245,8 +294,8 @@ public class FuzzTarget { } } - // Removes all stack trace elements that live in the Java standard library, internal JDK classes - // or the autofuzz package from the bottom of all stack frames. + // Removes all stack trace elements that live in the Java reflection packages or the autofuzz + // package from the bottom of all stack frames. private static void cleanStackTraces(Throwable t) { Throwable cause = t; while (cause != null) { @@ -255,8 +304,9 @@ public class FuzzTarget { for (firstInterestingPos = elements.length - 1; firstInterestingPos > 0; firstInterestingPos--) { String className = elements[firstInterestingPos].getClassName(); - if (!className.startsWith("com.code_intelligence.jazzer.autofuzz") - && !className.startsWith("java.") && !className.startsWith("jdk.")) { + if (!className.startsWith("com.code_intelligence.jazzer.autofuzz.") + && !className.startsWith("java.lang.reflect.") + && !className.startsWith("jdk.internal.reflect.")) { break; } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java index 96980530..3d48017f 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java @@ -36,9 +36,14 @@ import java.io.InputStream; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -46,11 +51,13 @@ import net.jodah.typetools.TypeResolver; import net.jodah.typetools.TypeResolver.Unknown; public class Meta { - static WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache = new WeakHashMap<>(); - static WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache = new WeakHashMap<>(); - static WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache = + static final WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache = new WeakHashMap<>(); + static final WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache = + new WeakHashMap<>(); + static final WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache = + new WeakHashMap<>(); + static final WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache = new WeakHashMap<>(); - static WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache = new WeakHashMap<>(); public static Object autofuzz(FuzzedDataProvider data, Method method) { return autofuzz(data, method, null); @@ -64,22 +71,29 @@ public class Meta { visitor.pushGroup( String.format("%s.", method.getDeclaringClass().getCanonicalName()), "", ""); } - result = autofuzz(data, method, null, visitor); - if (visitor != null) { - visitor.popGroup(); + try { + result = autofuzz(data, method, null, visitor); + } finally { + if (visitor != null) { + visitor.popGroup(); + } } } else { if (visitor != null) { // This group will always have two elements: The thisObject and the method call. - visitor.pushGroup("", ".", ""); + // Since the this object can be a complex expression, wrap it in paranthesis. + visitor.pushGroup("(", ").", ""); } Object thisObject = consume(data, method.getDeclaringClass(), visitor); if (thisObject == null) { throw new AutofuzzConstructionException(); } - result = autofuzz(data, method, thisObject, visitor); - if (visitor != null) { - visitor.popGroup(); + try { + result = autofuzz(data, method, thisObject, visitor); + } finally { + if (visitor != null) { + visitor.popGroup(); + } } } return result; @@ -210,7 +224,13 @@ public class Meta { return consume(data, type, null); } - static Object consume(FuzzedDataProvider data, Class<?> type, AutofuzzCodegenVisitor visitor) { + // Invariant: The Java source code representation of the returned object visited by visitor must + // represent an object of the same type as genericType. For example, a null value returned for + // the genericType Class<java.lang.String> should lead to the generated code + // "(java.lang.String) null", not just "null". This makes it possible to safely use consume in + // recursive argument constructions. + static Object consume(FuzzedDataProvider data, Type genericType, AutofuzzCodegenVisitor visitor) { + Class<?> type = getRawType(genericType); if (type == byte.class || type == Byte.class) { byte result = data.consumeByte(); if (visitor != null) @@ -252,13 +272,18 @@ public class Meta { visitor.addCharLiteral(result); return result; } - // Return null for non-primitive and non-boxed types in ~5% of the cases. + // Sometimes, but rarely return null for non-primitive and non-boxed types. // TODO: We might want to return null for boxed types sometimes, but this is complicated by the // fact that TypeUtils can't distinguish between a primitive type and its wrapper and may // thus easily cause false-positive NullPointerExceptions. - if (!type.isPrimitive() && data.consumeByte((byte) 0, (byte) 19) == 0) { - if (visitor != null) - visitor.pushElement("null"); + if (!type.isPrimitive() && data.consumeByte() == 0) { + if (visitor != null) { + if (type == Object.class) { + visitor.pushElement("null"); + } else { + visitor.pushElement(String.format("(%s) null", type.getCanonicalName())); + } + } return null; } if (type == String.class || type == CharSequence.class) { @@ -344,6 +369,60 @@ public class Meta { ", ", "new java.io.ByteArrayInputStream(new byte[]{", "})"))); } return new ByteArrayInputStream(array); + } else if (type == Map.class) { + ParameterizedType mapType = (ParameterizedType) genericType; + if (mapType.getActualTypeArguments().length != 2) { + throw new AutofuzzError( + "Expected Map generic type to have two type parameters: " + mapType); + } + Type keyType = mapType.getActualTypeArguments()[0]; + Type valueType = mapType.getActualTypeArguments()[1]; + if (visitor != null) { + // Do not use Collectors.toMap() since it cannot handle null values. + // Also annotate the type of the entry stream since it might be empty, in which case type + // inference on the accumulator could fail. + visitor.pushGroup( + String.format("java.util.stream.Stream.<java.util.AbstractMap.SimpleEntry<%s, %s>>of(", + keyType.getTypeName(), valueType.getTypeName()), + ", ", + ").collect(java.util.HashMap::new, (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll)"); + } + int remainingBytesBeforeFirstEntryCreation = data.remainingBytes(); + if (visitor != null) { + visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")"); + } + Object firstKey = consume(data, keyType, visitor); + Object firstValue = consume(data, valueType, visitor); + if (visitor != null) { + visitor.popGroup(); + } + int remainingBytesAfterFirstEntryCreation = data.remainingBytes(); + int sizeOfElementEstimate = + remainingBytesBeforeFirstEntryCreation - remainingBytesAfterFirstEntryCreation; + int mapSize = consumeArrayLength(data, sizeOfElementEstimate); + Map<Object, Object> map = new HashMap<>(mapSize); + for (int i = 0; i < mapSize; i++) { + if (i == 0) { + map.put(firstKey, firstValue); + } else { + if (visitor != null) { + visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")"); + } + map.put(consume(data, keyType, visitor), consume(data, valueType, visitor)); + if (visitor != null) { + visitor.popGroup(); + } + } + } + if (visitor != null) { + if (mapSize == 0) { + // We implicitly pushed the first entry with the call to consume above, but it is not + // part of the array. + visitor.popElement(); + } + visitor.popGroup(); + } + return map; } else if (type.isEnum()) { Enum<?> enumValue = (Enum<?>) data.pickValue(type.getEnumConstants()); if (visitor != null) { @@ -578,7 +657,7 @@ public class Meta { FuzzedDataProvider data, Executable executable, AutofuzzCodegenVisitor visitor) { Object[] result; try { - result = Arrays.stream(executable.getParameterTypes()) + result = Arrays.stream(executable.getGenericParameterTypes()) .map((type) -> consume(data, type, visitor)) .toArray(); return result; @@ -616,4 +695,22 @@ public class Meta { } return result; } + + private static Class<?> getRawType(Type genericType) { + if (genericType instanceof Class<?>) { + return (Class<?>) genericType; + } else if (genericType instanceof ParameterizedType) { + return getRawType(((ParameterizedType) genericType).getRawType()); + } else if (genericType instanceof WildcardType) { + // TODO: Improve this. + return Object.class; + } else if (genericType instanceof TypeVariable<?>) { + throw new AutofuzzError("Did not expect genericType to be a TypeVariable: " + genericType); + } else if (genericType instanceof GenericArrayType) { + // TODO: Improve this; + return Object[].class; + } else { + throw new AutofuzzError("Got unexpected class implementing Type: " + genericType); + } + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel deleted file mode 100644 index fceda64c..00000000 --- a/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel +++ /dev/null @@ -1,40 +0,0 @@ -java_binary( - name = "NoThrowDoclet", - srcs = ["NoThrowDoclet.java"], - create_executable = False, - tags = ["manual"], -) - -# To regenerate the list of methods, ensure that your local JDK is as recent as possible and contains `lib/src.zip`. -# This will be the case if you are using the release binaries of the OpenJDK or if the `openjdk-<version>-source` -# package is installed. -# Then, execute -# agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh -# from the Bazel root and copy the file into -# org.jacoco.core/src/org/jacoco/core/internal/flow/java_no_throw_methods_list.dat -# in the CodeIntelligenceTesting/jacoco repository. -genrule( - name = "java_no_throw_methods_list", - srcs = [ - "@local_jdk//:lib/src.zip", - ], - outs = [ - "java_no_throw_methods_list.dat.generated", - ], - cmd = """ - TMP=$$(mktemp -d) && \ - unzip $(execpath @local_jdk//:lib/src.zip) -d $$TMP && \ - $(execpath @local_jdk//:bin/javadoc) \ - -doclet com.code_intelligence.jazzer.generated.NoThrowDoclet \ - -docletpath $(execpath :NoThrowDoclet_deploy.jar) \ - --module java.base \ - --source-path $$TMP/java.base \ - --out $@ && \ - sort -o $@ $@ && \ - rm -rf $$TMP""", - tags = ["manual"], - tools = [ - ":NoThrowDoclet_deploy.jar", - "@local_jdk//:bin/javadoc", - ], -) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java b/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java deleted file mode 100644 index 1b52a228..00000000 --- a/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2021 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.generated; - -import com.sun.source.doctree.DocCommentTree; -import com.sun.source.doctree.DocTree; -import com.sun.source.doctree.ThrowsTree; -import com.sun.source.util.DocTrees; -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Collectors; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.ModuleElement; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.ArrayType; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementFilter; -import jdk.javadoc.doclet.Doclet; -import jdk.javadoc.doclet.DocletEnvironment; -import jdk.javadoc.doclet.Reporter; - -/** - * A Doclet that extracts a list of all method signatures in {@code java.*} that are declared not to - * throw any exceptions, including {@link RuntimeException} but excluding {@link - * VirtualMachineError}. - * - * Crucially, whereas the throws declaration of a method does not contain subclasses of {@link - * RuntimeException}, the {@code @throws} Javadoc tag does. - */ -public class NoThrowDoclet implements Doclet { - private BufferedWriter out; - - @Override - public void init(Locale locale, Reporter reporter) {} - - @Override - public String getName() { - return getClass().getSimpleName(); - } - - @Override - public Set<? extends Option> getSupportedOptions() { - return Set.of(new Option() { - @Override - public int getArgumentCount() { - return 1; - } - - @Override - public String getDescription() { - return "Output file (.kt)"; - } - - @Override - public Kind getKind() { - return Kind.STANDARD; - } - - @Override - public List<String> getNames() { - return List.of("--out"); - } - - @Override - public String getParameters() { - return "<output file (.kt)>"; - } - - @Override - public boolean process(String option, List<String> args) { - try { - out = new BufferedWriter(new FileWriter(args.get(0))); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } - }); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return null; - } - - private String toDescriptor(TypeMirror type) { - switch (type.getKind()) { - case BOOLEAN: - return "Z"; - case BYTE: - return "B"; - case CHAR: - return "C"; - case DOUBLE: - return "D"; - case FLOAT: - return "F"; - case INT: - return "I"; - case LONG: - return "J"; - case SHORT: - return "S"; - case VOID: - return "V"; - case ARRAY: - return "[" + toDescriptor(((ArrayType) type).getComponentType()); - case DECLARED: - return "L" + getFullyQualifiedName((DeclaredType) type) + ";"; - case TYPEVAR: - return "Ljava/lang/Object;"; - } - throw new IllegalArgumentException( - "Unexpected kind " + type.getKind() + ": " + type.toString()); - } - - private String getFullyQualifiedName(DeclaredType declaredType) { - TypeElement element = (TypeElement) declaredType.asElement(); - return element.getQualifiedName().toString().replace('.', '/'); - } - - private void handleExecutableElement(DocTrees trees, ExecutableElement executable) - throws IOException { - if (!executable.getModifiers().contains(Modifier.PUBLIC)) - return; - - DocCommentTree tree = trees.getDocCommentTree(executable); - if (tree != null) { - for (DocTree tag : tree.getBlockTags()) { - if (tag instanceof ThrowsTree) { - return; - } - } - } - - String methodName = executable.getSimpleName().toString(); - String className = - ((TypeElement) executable.getEnclosingElement()).getQualifiedName().toString(); - String internalClassName = className.replace('.', '/'); - - String paramTypeDescriptors = executable.getParameters() - .stream() - .map(VariableElement::asType) - .map(this::toDescriptor) - .collect(Collectors.joining("")); - String returnTypeDescriptor = toDescriptor(executable.getReturnType()); - String methodDescriptor = String.format("(%s)%s", paramTypeDescriptors, returnTypeDescriptor); - - out.write(String.format("%s#%s#%s%n", internalClassName, methodName, methodDescriptor)); - } - - public void handleTypeElement(DocTrees trees, TypeElement typeElement) throws IOException { - List<ExecutableElement> executables = - ElementFilter.constructorsIn(typeElement.getEnclosedElements()); - executables.addAll(ElementFilter.methodsIn(typeElement.getEnclosedElements())); - for (ExecutableElement executableElement : executables) { - handleExecutableElement(trees, executableElement); - } - } - - @Override - public boolean run(DocletEnvironment docletEnvironment) { - try { - DocTrees trees = docletEnvironment.getDocTrees(); - for (ModuleElement moduleElement : - ElementFilter.modulesIn(docletEnvironment.getSpecifiedElements())) { - for (PackageElement packageElement : - ElementFilter.packagesIn(moduleElement.getEnclosedElements())) { - if (packageElement.getQualifiedName().toString().startsWith("java.")) { - for (TypeElement typeElement : - ElementFilter.typesIn(packageElement.getEnclosedElements())) { - ElementKind kind = typeElement.getKind(); - if (kind == ElementKind.CLASS || kind == ElementKind.ENUM - || kind == ElementKind.INTERFACE) { - handleTypeElement(trees, typeElement); - } - } - } - } - } - } catch (IOException e) { - e.printStackTrace(); - return false; - } - try { - out.close(); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } -}
\ No newline at end of file diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 50d10705..db93dcae 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -12,34 +12,24 @@ kt_jvm_library( "Hook.kt", "HookInstrumentor.kt", "HookMethodVisitor.kt", + "Hooks.kt", "Instrumentor.kt", + "StaticMethodStrategy.java", "TraceDataFlowInstrumentor.kt", ], visibility = [ + "//agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__", "//agent/src/main/java/com/code_intelligence/jazzer/agent:__pkg__", "//agent/src/test/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", ], deps = [ - ":shaded_deps", "//agent/src/main/java/com/code_intelligence/jazzer/runtime", "//agent/src/main/java/com/code_intelligence/jazzer/utils", "@com_github_classgraph_classgraph//:classgraph", "@com_github_jetbrains_kotlin//:kotlin-reflect", - ], -) - -jar_jar( - name = "shaded_deps", - input_jar = "unshaded_deps_deploy.jar", - rules = "shade_rules", -) - -java_binary( - name = "unshaded_deps", - create_executable = False, - runtime_deps = [ "@jazzer_jacoco//:jacoco_internal", - "@jazzer_ow2_asm//:asm", - "@jazzer_ow2_asm//:asm_commons", + "@org_ow2_asm_asm//jar", + "@org_ow2_asm_asm_commons//jar", ], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt index f6728a1a..4c3eabcb 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt @@ -14,6 +14,8 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.runtime.CoverageMap + fun extractClassFileMajorVersion(classfileBuffer: ByteArray): Int { return ((classfileBuffer[6].toInt() and 0xff) shl 8) or (classfileBuffer[7].toInt() and 0xff) } @@ -24,7 +26,11 @@ class ClassInstrumentor constructor(bytecode: ByteArray) { private set fun coverage(initialEdgeId: Int): Int { - val edgeCoverageInstrumentor = EdgeCoverageInstrumentor(initialEdgeId) + val edgeCoverageInstrumentor = EdgeCoverageInstrumentor( + defaultEdgeCoverageStrategy, + defaultCoverageMap, + initialEdgeId, + ) instrumentedBytecode = edgeCoverageInstrumentor.instrument(instrumentedBytecode) return edgeCoverageInstrumentor.numEdges } @@ -41,13 +47,7 @@ class ClassInstrumentor constructor(bytecode: ByteArray) { } companion object { - init { - try { - // Calls JNI_OnLoad_jazzer_initialize in the driver, which registers the native methods. - System.loadLibrary("jazzer_initialize") - } catch (_: UnsatisfiedLinkError) { - // Make it possible to use (parts of) the agent without the driver. - } - } + val defaultEdgeCoverageStrategy = StaticMethodStrategy() + val defaultCoverageMap = CoverageMap::class.java } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt index 65956189..098cf389 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt @@ -15,18 +15,17 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.runtime.CoverageMap -import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.CoverageBuilder -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionData -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataReader -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataStore -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataWriter -import com.code_intelligence.jazzer.third_party.jacoco.core.data.SessionInfo -import com.code_intelligence.jazzer.third_party.jacoco.core.data.SessionInfoStore -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.data.CRC64 +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.CoverageBuilder +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionData +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataWriter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.SessionInfo +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.data.CRC64 import com.code_intelligence.jazzer.utils.ClassNameGlobber import io.github.classgraph.ClassGraph -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream import java.time.Instant import java.util.UUID @@ -52,26 +51,26 @@ object CoverageRecorder { } /** - * Manually records coverage IDs based on the current state of [CoverageMap.mem]. + * Manually records coverage IDs based on the current state of [CoverageMap]. * Should be called after static initializers have run. */ @JvmStatic fun updateCoveredIdsWithCoverageMap() { - val mem = CoverageMap.mem - val size = mem.capacity() - additionalCoverage.addAll((0 until size).filter { mem[it] > 0 }) + additionalCoverage.addAll(CoverageMap.getCoveredIds()) } + /** + * [dumpCoverageReport] dumps a human-readable coverage report of files using any [coveredIds] to [dumpFileName]. + */ @JvmStatic - fun replayCoveredIds() { - val mem = CoverageMap.mem - for (coverageId in additionalCoverage) { - mem.put(coverageId, 1) + fun dumpCoverageReport(coveredIds: IntArray, dumpFileName: String) { + File(dumpFileName).bufferedWriter().use { writer -> + writer.write(computeFileCoverage(coveredIds)) } } - @JvmStatic - fun computeFileCoverage(coveredIds: IntArray): String { + private fun computeFileCoverage(coveredIds: IntArray): String { + fun Double.format(digits: Int) = "%.${digits}f".format(this) val coverage = analyzeCoverage(coveredIds.toSet()) ?: return "No classes were instrumented" return coverage.sourceFiles.joinToString( "\n", @@ -109,21 +108,42 @@ object CoverageRecorder { } } - private fun Double.format(digits: Int) = "%.${digits}f".format(this) + /** + * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [dumpFileName]. + * JaCoCo only exports coverage for files containing at least one coverage data point. The dump + * can be used by the JaCoCo report command to create reports also including not covered files. + */ + @JvmStatic + fun dumpJacocoCoverage(coveredIds: IntArray, dumpFileName: String) { + FileOutputStream(dumpFileName).use { outStream -> + dumpJacocoCoverage(coveredIds, outStream) + } + } + + /** + * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [outStream]. + */ + @JvmStatic + fun dumpJacocoCoverage(coveredIds: IntArray, outStream: OutputStream) { + // Return if no class has been instrumented. + val startTimestamp = startTimestamp ?: return - fun dumpJacocoCoverage(coveredIds: Set<Int>): ByteArray? { // Update the list of covered IDs with the coverage information for the current run. updateCoveredIdsWithCoverageMap() val dumpTimestamp = Instant.now() - val outStream = ByteArrayOutputStream() val outWriter = ExecutionDataWriter(outStream) - // Return null if no class has been instrumented. - val startTimestamp = startTimestamp ?: return null outWriter.visitSessionInfo( SessionInfo(UUID.randomUUID().toString(), startTimestamp.epochSecond, dumpTimestamp.epochSecond) ) + analyzeJacocoCoverage(coveredIds.toSet()).accept(outWriter) + } + /** + * Build up a JaCoCo [ExecutionDataStore] based on [coveredIds] containing the internally gathered coverage information. + */ + private fun analyzeJacocoCoverage(coveredIds: Set<Int>): ExecutionDataStore { + val executionDataStore = ExecutionDataStore() val sortedCoveredIds = (additionalCoverage + coveredIds).sorted().toIntArray() for ((internalClassName, info) in instrumentedClassInfo) { // Determine the subarray of coverage IDs in sortedCoveredIds that contains the IDs generated while @@ -153,32 +173,27 @@ object CoverageRecorder { .forEach { classLocalEdgeId -> probes[classLocalEdgeId] = true } - outWriter.visitClassExecution(ExecutionData(info.classId, internalClassName, probes)) + executionDataStore.visitClassExecution(ExecutionData(info.classId, internalClassName, probes)) } - return outStream.toByteArray() + return executionDataStore } + /** + * Create a [CoverageBuilder] containing all classes matching the include/exclude pattern and their coverage statistics. + */ fun analyzeCoverage(coveredIds: Set<Int>): CoverageBuilder? { return try { val coverage = CoverageBuilder() analyzeAllUncoveredClasses(coverage) - val rawExecutionData = dumpJacocoCoverage(coveredIds) ?: return null - val executionDataStore = ExecutionDataStore() - val sessionInfoStore = SessionInfoStore() - ByteArrayInputStream(rawExecutionData).use { stream -> - ExecutionDataReader(stream).run { - setExecutionDataVisitor(executionDataStore) - setSessionInfoVisitor(sessionInfoStore) - read() - } - } + val executionDataStore = analyzeJacocoCoverage(coveredIds) for ((internalClassName, info) in instrumentedClassInfo) { - EdgeCoverageInstrumentor(0).analyze( - executionDataStore, - coverage, - info.bytecode, - internalClassName - ) + EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0) + .analyze( + executionDataStore, + coverage, + info.bytecode, + internalClassName + ) } coverage } catch (e: Exception) { @@ -198,7 +213,6 @@ object CoverageRecorder { .asSequence() .map { it.replace('/', '.') } .toSet() - val emptyExecutionDataStore = ExecutionDataStore() ClassGraph() .enableClassInfo() .ignoreClassVisibility() @@ -209,13 +223,16 @@ object CoverageRecorder { "jaz", ) .scan().use { result -> + // ExecutionDataStore is used to look up existing coverage during analysis of the class files, + // no entries are added during that. Passing in an empty store is fine for uncovered files. + val emptyExecutionDataStore = ExecutionDataStore() result.allClasses .asSequence() .filter { classInfo -> classNameGlobber.includes(classInfo.name) } .filterNot { classInfo -> classInfo.name in coveredClassNames } .forEach { classInfo -> classInfo.resource.use { resource -> - EdgeCoverageInstrumentor(0).analyze( + EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0).analyze( emptyExecutionDataStore, coverage, resource.load(), diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt index ba5b7ee9..8fb3dc2b 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt @@ -14,37 +14,92 @@ package com.code_intelligence.jazzer.instrumentor -import com.code_intelligence.jazzer.runtime.CoverageMap -import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.Analyzer -import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.ICoverageVisitor -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataStore -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.ClassProbesAdapter -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.ClassProbesVisitor -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.IClassProbesAdapterFactory -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.JavaNoThrowMethods -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.ClassInstrumenter -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.IProbeArrayStrategy -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.IProbeInserterFactory -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.InstrSupport -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.ProbeInserter -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter -import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.Analyzer +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.ICoverageVisitor +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesAdapter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesVisitor +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.IClassProbesAdapterFactory +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ClassInstrumenter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeArrayStrategy +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeInserterFactory +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.InstrSupport +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ProbeInserter +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles.publicLookup +import java.lang.invoke.MethodType.methodType import kotlin.math.max +/** + * A particular way to instrument bytecode for edge coverage using a coverage map class available to + * hold the collected coverage data at runtime. + */ +interface EdgeCoverageStrategy { + + /** + * Inject bytecode instrumentation on a control flow edge with ID [edgeId], with access to the + * local variable [variable] that is populated at the beginning of each method by the + * instrumentation injected in [loadLocalVariable]. + */ + fun instrumentControlFlowEdge( + mv: MethodVisitor, + edgeId: Int, + variable: Int, + coverageMapInternalClassName: String + ) + + /** + * The maximal number of stack elements used by [instrumentControlFlowEdge]. + */ + val instrumentControlFlowEdgeStackSize: Int + + /** + * The type of the local variable used by the instrumentation in the format used by + * [MethodVisitor.visitFrame]'s `local` parameter, or `null` if the instrumentation does not use + * one. + * @see https://asm.ow2.io/javadoc/org/objectweb/asm/MethodVisitor.html#visitFrame(int,int,java.lang.Object%5B%5D,int,java.lang.Object%5B%5D) + */ + val localVariableType: Any? + + /** + * Inject bytecode that loads the coverage counters of the coverage map class described by + * [coverageMapInternalClassName] into the local variable [variable]. + */ + fun loadLocalVariable(mv: MethodVisitor, variable: Int, coverageMapInternalClassName: String) + + /** + * The maximal number of stack elements used by [loadLocalVariable]. + */ + val loadLocalVariableStackSize: Int +} + +// An instance of EdgeCoverageInstrumentor should only be used to instrument a single class as it +// internally tracks the edge IDs, which have to be globally unique. class EdgeCoverageInstrumentor( + private val strategy: EdgeCoverageStrategy, + /** + * The class must have the following public static member + * - method enlargeIfNeeded(int nextEdgeId): Called before a new edge ID is emitted. + */ + coverageMapClass: Class<*>, private val initialEdgeId: Int, - private val coverageMapClass: Class<*> = CoverageMap::class.java ) : Instrumentor { private var nextEdgeId = initialEdgeId + private val coverageMapInternalClassName = coverageMapClass.name.replace('.', '/') - init { - if (isTesting) { - JavaNoThrowMethods.isTesting = true - } - } + private val enlargeIfNeeded: MethodHandle = + publicLookup().findStatic( + coverageMapClass, + "enlargeIfNeeded", + methodType( + Void::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + ) override fun instrument(bytecode: ByteArray): ByteArray { val reader = InstrSupport.classReaderFor(bytecode) @@ -67,93 +122,14 @@ class EdgeCoverageInstrumentor( val numEdges get() = nextEdgeId - initialEdgeId - private val isTesting - get() = coverageMapClass != CoverageMap::class.java - private fun nextEdgeId(): Int { - if (nextEdgeId >= CoverageMap.mem.capacity()) { - if (!isTesting) { - CoverageMap.enlargeCoverageMap() - } - } + enlargeIfNeeded.invokeExact(nextEdgeId) return nextEdgeId++ } /** - * The maximal number of stack elements used by [loadCoverageMap]. - */ - private val loadCoverageMapStackSize = 1 - - /** - * Inject bytecode that loads the coverage map into local variable [variable]. - */ - private fun loadCoverageMap(mv: MethodVisitor, variable: Int) { - mv.apply { - visitFieldInsn( - Opcodes.GETSTATIC, - coverageMapInternalClassName, - "mem", - "Ljava/nio/ByteBuffer;" - ) - // Stack: mem (maxStack: 1) - visitVarInsn(Opcodes.ASTORE, variable) - } - } - - /** - * The maximal number of stack elements used by [instrumentControlFlowEdge]. - */ - private val instrumentControlFlowEdgeStackSize = 5 - - /** - * Inject bytecode instrumentation on a control flow edge with ID [edgeId]. The coverage map can be loaded from - * local variable [variable]. - */ - private fun instrumentControlFlowEdge(mv: MethodVisitor, edgeId: Int, variable: Int) { - mv.apply { - visitVarInsn(Opcodes.ALOAD, variable) - // Stack: mem - push(edgeId) - // Stack: mem | edgeId - visitInsn(Opcodes.DUP2) - // Stack: mem | edgeId | mem | 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: mem | edgeId | counter (sign-extended to int) - push(0xff) - // Stack: mem | edgeId | counter (sign-extended to int) | 0x000000ff - visitInsn(Opcodes.IAND) - // Stack: mem | edgeId | counter (zero-extended to int) - push(1) - // Stack: mem | edgeId | counter | 1 - visitInsn(Opcodes.IADD) - // Stack: mem | edgeId | counter + 1 - visitInsn(Opcodes.DUP) - // Stack: mem | edgeId | counter + 1 | counter + 1 - push(8) - // Stack: mem | edgeId | counter + 1 | counter + 1 | 8 (maxStack: +5) - visitInsn(Opcodes.ISHR) - // Stack: mem | edgeId | counter + 1 | 1 if the increment overflowed to 0, 0 otherwise - visitInsn(Opcodes.IADD) - // Stack: mem | edgeId | counter + 2 if the increment overflowed, counter + 1 otherwise - visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "put", "(IB)Ljava/nio/ByteBuffer;", false) - // Stack: mem - visitInsn(Opcodes.POP) - if (isTesting) { - visitMethodInsn(Opcodes.INVOKESTATIC, coverageMapInternalClassName, "updated", "()V", false) - } - } - } - -// The remainder of this file interfaces with classes in org.jacoco.core.internal. Changes to this part should not be -// necessary unless JaCoCo is updated or the way we instrument for coverage changes fundamentally. - - /** - * A [ProbeInserter] that injects the bytecode instrumentation returned by [instrumentControlFlowEdge] and modifies - * the stack size and number of local variables accordingly. + * A [ProbeInserter] that injects bytecode instrumentation at every control flow edge and + * modifies the stack size and number of local variables accordingly. */ private inner class EdgeCoverageProbeInserter( access: Int, @@ -163,13 +139,16 @@ class EdgeCoverageInstrumentor( arrayStrategy: IProbeArrayStrategy, ) : ProbeInserter(access, name, desc, mv, arrayStrategy) { override fun insertProbe(id: Int) { - instrumentControlFlowEdge(mv, id, variable) + strategy.instrumentControlFlowEdge(mv, id, variable, coverageMapInternalClassName) } override fun visitMaxs(maxStack: Int, maxLocals: Int) { - val newMaxStack = max(maxStack + instrumentControlFlowEdgeStackSize, loadCoverageMapStackSize) - mv.visitMaxs(newMaxStack, maxLocals + 1) + val newMaxStack = max(maxStack + strategy.instrumentControlFlowEdgeStackSize, strategy.loadLocalVariableStackSize) + val newMaxLocals = maxLocals + if (strategy.localVariableType != null) 1 else 0 + mv.visitMaxs(newMaxStack, newMaxLocals) } + + override fun getLocalVariableType() = strategy.localVariableType } private val edgeCoverageProbeInserterFactory = @@ -177,9 +156,16 @@ class EdgeCoverageInstrumentor( EdgeCoverageProbeInserter(access, name, desc, mv, arrayStrategy) } - private inner class EdgeCoverageClassProbesAdapter(cv: ClassProbesVisitor, trackFrames: Boolean) : - ClassProbesAdapter(cv, trackFrames) { + private inner class EdgeCoverageClassProbesAdapter(private val cpv: ClassProbesVisitor, trackFrames: Boolean) : + ClassProbesAdapter(cpv, trackFrames) { override fun nextId(): Int = nextEdgeId() + + override fun visitEnd() { + cpv.visitTotalProbeCount(numEdges) + // Avoid calling super.visitEnd() as that invokes cpv.visitTotalProbeCount with an + // incorrect value of `count`. + cv.visitEnd() + } } private val edgeCoverageClassProbesAdapterFactory = IClassProbesAdapterFactory { probesVisitor, trackFrames -> @@ -188,14 +174,14 @@ class EdgeCoverageInstrumentor( private val edgeCoverageProbeArrayStrategy = object : IProbeArrayStrategy { override fun storeInstance(mv: MethodVisitor, clinit: Boolean, variable: Int): Int { - loadCoverageMap(mv, variable) - return loadCoverageMapStackSize + strategy.loadLocalVariable(mv, variable, coverageMapInternalClassName) + return strategy.loadLocalVariableStackSize } override fun addMembers(cv: ClassVisitor, probeCount: Int) {} } +} - private fun MethodVisitor.push(value: Int) { - InstrSupport.push(this, value) - } +fun MethodVisitor.push(value: Int) { + InstrSupport.push(this, value) } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt index 92106e14..ff68ad94 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt @@ -18,46 +18,65 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.api.HookType import com.code_intelligence.jazzer.api.MethodHook -import com.code_intelligence.jazzer.api.MethodHooks import com.code_intelligence.jazzer.utils.descriptor import java.lang.invoke.MethodHandle import java.lang.reflect.Method import java.lang.reflect.Modifier -class Hook private constructor(hookMethod: Method, annotation: MethodHook) { - // Allowing arbitrary exterior whitespace in the target class name allows for an easy workaround - // for mangled hooks due to shading applied to hooks. - private val targetClassName = annotation.targetClassName.trim() - val targetMethodName = annotation.targetMethod - val targetMethodDescriptor = annotation.targetMethodDescriptor.takeIf { it.isNotEmpty() } - val hookType = annotation.type - - val targetInternalClassName = targetClassName.replace('.', '/') - private val targetReturnTypeDescriptor = targetMethodDescriptor?.let { extractReturnTypeDescriptor(it) } - private val targetWrappedReturnTypeDescriptor = targetReturnTypeDescriptor?.let { getWrapperTypeDescriptor(it) } - - private val hookClassName: String = hookMethod.declaringClass.name - val hookInternalClassName = hookClassName.replace('.', '/') - val hookMethodName: String = hookMethod.name - val hookMethodDescriptor = hookMethod.descriptor +class Hook private constructor( + private val targetClassName: String, + val hookType: HookType, + val targetMethodName: String, + val targetMethodDescriptor: String?, + val additionalClassesToHook: List<String>, + val targetInternalClassName: String, + private val targetReturnTypeDescriptor: String?, + private val targetWrappedReturnTypeDescriptor: String?, + private val hookClassName: String, + val hookInternalClassName: String, + val hookMethodName: String, + val hookMethodDescriptor: String +) { override fun toString(): String { - return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName" + return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName $additionalClassesToHook" } companion object { + fun createAndVerifyHook(hookMethod: Method, hookData: MethodHook, className: String): Hook { + return createHook(hookMethod, hookData, className).also { + verify(hookMethod, it) + } + } - fun verifyAndGetHook(hookMethod: Method, hookData: MethodHook): Hook { - // Verify the annotation type and extract information for debug statements. - val potentialHook = Hook(hookMethod, hookData) + private fun createHook(hookMethod: Method, annotation: MethodHook, targetClassName: String): Hook { + val targetReturnTypeDescriptor = annotation.targetMethodDescriptor + .takeIf { it.isNotBlank() }?.let { extractReturnTypeDescriptor(it) } + val hookClassName: String = hookMethod.declaringClass.name + return Hook( + targetClassName = targetClassName, + hookType = annotation.type, + targetMethodName = annotation.targetMethod, + targetMethodDescriptor = annotation.targetMethodDescriptor.takeIf { it.isNotBlank() }, + additionalClassesToHook = annotation.additionalClassesToHook.asList(), + targetInternalClassName = targetClassName.replace('.', '/'), + targetReturnTypeDescriptor = targetReturnTypeDescriptor, + targetWrappedReturnTypeDescriptor = targetReturnTypeDescriptor?.let { getWrapperTypeDescriptor(it) }, + hookClassName = hookClassName, + hookInternalClassName = hookClassName.replace('.', '/'), + hookMethodName = hookMethod.name, + hookMethodDescriptor = hookMethod.descriptor + ) + } + private fun verify(hookMethod: Method, potentialHook: Hook) { // Verify the hook method's modifiers (public static). require(Modifier.isPublic(hookMethod.modifiers)) { "$potentialHook: hook method must be public" } require(Modifier.isStatic(hookMethod.modifiers)) { "$potentialHook: hook method must be static" } // Verify the hook method's parameter count. val numParameters = hookMethod.parameters.size - when (hookData.type) { + when (potentialHook.hookType) { HookType.BEFORE, HookType.REPLACE -> require(numParameters == 4) { "$potentialHook: incorrect number of parameters (expected 4)" } HookType.AFTER -> require(numParameters == 5) { "$potentialHook: incorrect number of parameters (expected 5)" } } @@ -70,17 +89,18 @@ class Hook private constructor(hookMethod: Method, annotation: MethodHook) { require(parameterTypes[3] == Int::class.javaPrimitiveType) { "$potentialHook: fourth parameter must have type int" } // Verify the hook method's return type if possible. - when (hookData.type) { + when (potentialHook.hookType) { HookType.BEFORE, HookType.AFTER -> require(hookMethod.returnType == Void.TYPE) { "$potentialHook: return type must be void" } HookType.REPLACE -> if (potentialHook.targetReturnTypeDescriptor != null) { - val returnTypeDescriptor = hookMethod.returnType.descriptor - if (potentialHook.targetReturnTypeDescriptor == "V") { - require(returnTypeDescriptor == "V") { "$potentialHook: return type must be void to match targetMethodDescriptor" } + if (potentialHook.targetMethodName == "<init>") { + require(hookMethod.returnType.name == potentialHook.targetClassName) { "$potentialHook: return type must be ${potentialHook.targetClassName} to match target constructor" } + } else if (potentialHook.targetReturnTypeDescriptor == "V") { + require(hookMethod.returnType.descriptor == "V") { "$potentialHook: return type must be void" } } else { require( - returnTypeDescriptor in listOf( + hookMethod.returnType.descriptor in listOf( java.lang.Object::class.java.descriptor, potentialHook.targetReturnTypeDescriptor, potentialHook.targetWrappedReturnTypeDescriptor @@ -92,28 +112,22 @@ class Hook private constructor(hookMethod: Method, annotation: MethodHook) { } } - // AfterMethodHook only: Verify the type of the last parameter if known. - if (hookData.type == HookType.AFTER && potentialHook.targetReturnTypeDescriptor != null) { - require( - parameterTypes[4] == java.lang.Object::class.java || - parameterTypes[4].descriptor == potentialHook.targetWrappedReturnTypeDescriptor - ) { - "$potentialHook: fifth parameter must have type Object or match the descriptor ${potentialHook.targetWrappedReturnTypeDescriptor}" + // AfterMethodHook only: Verify the type of the last parameter if known. Even if not + // known, it must not be a primitive value. + if (potentialHook.hookType == HookType.AFTER) { + if (potentialHook.targetReturnTypeDescriptor != null) { + require( + parameterTypes[4] == java.lang.Object::class.java || + parameterTypes[4].descriptor == potentialHook.targetWrappedReturnTypeDescriptor + ) { + "$potentialHook: fifth parameter must have type Object or match the descriptor ${potentialHook.targetWrappedReturnTypeDescriptor}" + } + } else { + require(!parameterTypes[4].isPrimitive) { + "$potentialHook: fifth parameter must not be a primitive type, use a boxed type instead" + } } } - - return potentialHook - } - } -} - -fun loadHooks(hookClass: Class<*>): List<Hook> { - val hooks = mutableListOf<Hook>() - for (method in hookClass.methods) { - method.getAnnotation(MethodHook::class.java)?.let { hooks.add(Hook.verifyAndGetHook(method, it)) } - method.getAnnotation(MethodHooks::class.java)?.let { - it.value.forEach { hookAnnotation -> hooks.add(Hook.verifyAndGetHook(method, hookAnnotation)) } } } - return hooks } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt index ac5f1780..6db76605 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt @@ -14,10 +14,10 @@ package com.code_intelligence.jazzer.instrumentor -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter -import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor internal class HookInstrumentor(private val hooks: Iterable<Hook>, private val java6Mode: Boolean) : Instrumentor { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt index 7c23c703..1694be58 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt @@ -15,11 +15,12 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.api.HookType -import com.code_intelligence.jazzer.third_party.objectweb.asm.Handle -import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes -import com.code_intelligence.jazzer.third_party.objectweb.asm.Type -import com.code_intelligence.jazzer.third_party.objectweb.asm.commons.LocalVariablesSorter +import org.objectweb.asm.Handle +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.commons.LocalVariablesSorter +import java.util.concurrent.atomic.AtomicBoolean internal fun makeHookMethodVisitor( access: Int, @@ -41,6 +42,10 @@ private class HookMethodVisitor( private val random: DeterministicRandom, ) : MethodVisitor(Instrumentor.ASM_API_VERSION, methodVisitor) { + companion object { + private val showUnsupportedHookWarning = AtomicBoolean(true) + } + val lvs = object : LocalVariablesSorter(Instrumentor.ASM_API_VERSION, access, descriptor, this) { override fun updateNewLocals(newLocals: Array<Any>) { // The local variables involved in calling hooks do not need to outlive the current @@ -51,7 +56,7 @@ private class HookMethodVisitor( } } - private val hooks = hooks.associateBy { hook -> + private val hooks = hooks.groupBy { hook -> var hookKey = "${hook.hookType}#${hook.targetInternalClassName}#${hook.targetMethodName}" if (hook.targetMethodDescriptor != null) hookKey += "#${hook.targetMethodDescriptor}" @@ -69,63 +74,23 @@ private class HookMethodVisitor( mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) return } - handleMethodInsn(HookType.BEFORE, opcode, owner, methodName, methodDescriptor, isInterface) - } - - /** - * Emits the bytecode for a method call instruction for the next applicable hook type in order (BEFORE, REPLACE, - * AFTER). Since the instrumented code is indistinguishable from an uninstrumented call instruction, it can be - * safely nested. Combining REPLACE hooks with other hooks is however not supported as these hooks already subsume - * the functionality of BEFORE and AFTER hooks. - */ - private fun visitNextHookTypeOrCall( - hookType: HookType, - appliedHook: Boolean, - opcode: Int, - owner: String, - methodName: String, - methodDescriptor: String, - isInterface: Boolean, - ) = when (hookType) { - HookType.BEFORE -> { - val nextHookType = if (appliedHook) { - // After a BEFORE hook has been applied, we can safely apply an AFTER hook by replacing the actual - // call instruction with the full bytecode injected for the AFTER hook. - HookType.AFTER - } else { - // If no BEFORE hook is registered, look for a REPLACE hook next. - HookType.REPLACE - } - handleMethodInsn(nextHookType, opcode, owner, methodName, methodDescriptor, isInterface) - } - HookType.REPLACE -> { - // REPLACE hooks can't (and don't need to) be mixed with other hooks. We only cycle through them if we - // couldn't find a matching REPLACE hook, in which case we try an AFTER hook next. - require(!appliedHook) - handleMethodInsn(HookType.AFTER, opcode, owner, methodName, methodDescriptor, isInterface) - } - // An AFTER hook is always the last in the chain. Whether a hook has been applied or not, always emit the - // actual call instruction. - HookType.AFTER -> mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + handleMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) } fun handleMethodInsn( - hookType: HookType, opcode: Int, owner: String, methodName: String, methodDescriptor: String, isInterface: Boolean, ) { - val hook = findMatchingHook(hookType, owner, methodName, methodDescriptor) - if (hook == null) { - visitNextHookTypeOrCall(hookType, false, opcode, owner, methodName, methodDescriptor, isInterface) + val matchingHooks = findMatchingHooks(owner, methodName, methodDescriptor) + + if (matchingHooks.isEmpty()) { + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) return } - // The hookId is used to identify a call site. - val hookId = random.nextInt() - val paramDescriptors = extractParameterTypeDescriptors(methodDescriptor) val localObjArr = storeMethodArguments(paramDescriptors) // If the method we're hooking is not static there is now a reference to @@ -142,138 +107,158 @@ private class HookMethodVisitor( // We now removed all values for the original method call from the operand stack // and saved them to local variables. - // Start to build the arguments for the hook method. - if (methodName == "<init>") { - // Special case for constructors: - // We cannot create a MethodHandle for a constructor, so we push null instead. - mv.visitInsn(Opcodes.ACONST_NULL) // push nullref - // Only pass the this object if it has been initialized by the time the hook is invoked. - if (hook.hookType == HookType.AFTER) { - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) - } else { - mv.visitInsn(Opcodes.ACONST_NULL) // push nullref - } - } else { - // Push a MethodHandle representing the hooked method. - val handleOpcode = when (opcode) { - Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL - Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE - Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC - Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL - else -> -1 - } - if (java6Mode) { - // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50). + val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor) + // Create a local variable to store the return value + val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor))) + + matchingHooks.forEachIndexed { index, hook -> + // The hookId is used to identify a call site. + val hookId = random.nextInt() + + // Start to build the arguments for the hook method. + if (methodName == "<init>") { + // Constructor is invoked on an uninitialized object, and that's still on the stack. + // In case of REPLACE pop it from the stack and replace it afterwards with the returned + // one from the hook. + if (hook.hookType == HookType.REPLACE) { + mv.visitInsn(Opcodes.POP) + } + // Special case for constructors: + // We cannot create a MethodHandle for a constructor, so we push null instead. mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + // Only pass the this object if it has been initialized by the time the hook is invoked. + if (hook.hookType == HookType.AFTER) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) + } else { + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } } else { - mv.visitLdcInsn( - Handle( - handleOpcode, - owner, - methodName, - methodDescriptor, - isInterface - ) - ) // push MethodHandle - } - // Stack layout: ... | MethodHandle (objectref) - // Push the owner object again - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) - } - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) - // Push a reference to our object array with the saved arguments - mv.visitVarInsn(Opcodes.ALOAD, localObjArr) - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) - // Push the hook id - mv.visitLdcInsn(hookId) - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) - // How we proceed depends on the type of hook we want to implement - when (hook.hookType) { - HookType.BEFORE -> { - // Call the hook method - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - hook.hookInternalClassName, - hook.hookMethodName, - hook.hookMethodDescriptor, - false - ) - // Stack layout: ... - // Push the values for the original method call onto the stack again - if (opcode != Opcodes.INVOKESTATIC) { - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object + // Push a MethodHandle representing the hooked method. + val handleOpcode = when (opcode) { + Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL + Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE + Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC + Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL + else -> -1 } - loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments - // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... - // Call the original method or the next hook in order. - visitNextHookTypeOrCall(hookType, true, opcode, owner, methodName, methodDescriptor, isInterface) + if (java6Mode) { + // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50). + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } else { + mv.visitLdcInsn( + Handle( + handleOpcode, + owner, + methodName, + methodDescriptor, + isInterface + ) + ) // push MethodHandle + } + // Stack layout: ... | MethodHandle (objectref) + // Push the owner object again + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) } - HookType.REPLACE -> { - // Call the hook method - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - hook.hookInternalClassName, - hook.hookMethodName, - hook.hookMethodDescriptor, - false - ) - // Stack layout: ... | [return value (primitive/objectref)] - // Check if we need to process the return value - val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor) - if (returnTypeDescriptor != "V") { - val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor) - // if the hook method's return type is primitive we don't need to unwrap or cast it - if (!isPrimitiveType(hookMethodReturnType)) { - // Check if the returned object type is different than the one that should be returned - // If a primitive should be returned we check it's wrapper type - val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor) - if (expectedType != hookMethodReturnType) { - // Cast object - mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType)) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) + // Push a reference to our object array with the saved arguments + mv.visitVarInsn(Opcodes.ALOAD, localObjArr) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) + // Push the hook id + mv.visitLdcInsn(hookId) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // How we proceed depends on the type of hook we want to implement + when (hook.hookType) { + HookType.BEFORE -> { + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false + ) + + // Call the original method if this is the last BEFORE hook. If not, the original method will be + // called by the next AFTER hook. + if (index == matchingHooks.lastIndex) { + // Stack layout: ... + // Push the values for the original method call onto the stack again + if (opcode != Opcodes.INVOKESTATIC) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object } - // Check if we need to unwrap the returned object - unwrapTypeIfPrimitive(returnTypeDescriptor) + loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments + // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) } } - } - HookType.AFTER -> { - // Push the values for the original method call again onto the stack - if (opcode != Opcodes.INVOKESTATIC) { - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object - } - loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) - // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... - // Call the original method or the next hook in order - visitNextHookTypeOrCall(hookType, true, opcode, owner, methodName, methodDescriptor, isInterface) - val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor) - if (returnTypeDescriptor == "V") { - // If the method didn't return anything, we push a nullref as placeholder - mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + HookType.REPLACE -> { + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false + ) + // Stack layout: ... | [return value (primitive/objectref)] + // Check if we need to process the return value + if (returnTypeDescriptor != "V") { + val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor) + // if the hook method's return type is primitive we don't need to unwrap or cast it + if (!isPrimitiveType(hookMethodReturnType)) { + // Check if the returned object type is different than the one that should be returned + // If a primitive should be returned we check it's wrapper type + val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor) + if (expectedType != hookMethodReturnType) { + // Cast object + mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType)) + } + // Check if we need to unwrap the returned object + unwrapTypeIfPrimitive(returnTypeDescriptor) + } + } } - // Wrap return value if it is a primitive type - wrapTypeIfPrimitive(returnTypeDescriptor) - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) - // | return value (objectref) - // Store the result value in a local variable (but keep it on the stack) - val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor))) - mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref - mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref - // Call the hook method - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - hook.hookInternalClassName, - hook.hookMethodName, - hook.hookMethodDescriptor, - false - ) - // Stack layout: ... - if (returnTypeDescriptor != "V") { - // Push the return value again + HookType.AFTER -> { + // Call the original method before the first AFTER hook + if (index == 0 || matchingHooks[index - 1].hookType != HookType.AFTER) { + // Push the values for the original method call again onto the stack + if (opcode != Opcodes.INVOKESTATIC) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object + } + loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + if (returnTypeDescriptor == "V") { + // If the method didn't return anything, we push a nullref as placeholder + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } + // Wrap return value if it is a primitive type + wrapTypeIfPrimitive(returnTypeDescriptor) + mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref + } mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref - // Unwrap it, if it was a primitive value - unwrapTypeIfPrimitive(returnTypeDescriptor) - // Stack layout: ... | return value (primitive/objectref) + + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // | return value (objectref) + // Store the result value in a local variable (but keep it on the stack) + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false + ) + // Stack layout: ... + // Push the return value on the stack after the last AFTER hook if the original method returns a value + if (index == matchingHooks.size - 1 && returnTypeDescriptor != "V") { + // Push the return value again + mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref + // Unwrap it, if it was a primitive value + unwrapTypeIfPrimitive(returnTypeDescriptor) + // Stack layout: ... | return value (primitive/objectref) + } } } } @@ -286,10 +271,38 @@ private class HookMethodVisitor( Opcodes.INVOKESPECIAL ) - private fun findMatchingHook(hookType: HookType, owner: String, name: String, descriptor: String): Hook? { - val withoutDescriptorKey = "$hookType#$owner#$name" - val withDescriptorKey = "$withoutDescriptorKey#$descriptor" - return hooks[withDescriptorKey] ?: hooks[withoutDescriptorKey] + private fun findMatchingHooks(owner: String, name: String, descriptor: String): List<Hook> { + val result = HookType.values().flatMap { hookType -> + val withoutDescriptorKey = "$hookType#$owner#$name" + val withDescriptorKey = "$withoutDescriptorKey#$descriptor" + hooks[withDescriptorKey].orEmpty() + hooks[withoutDescriptorKey].orEmpty() + }.sortedBy { it.hookType } + val replaceHookCount = result.count { it.hookType == HookType.REPLACE } + check( + replaceHookCount == 0 || + (replaceHookCount == 1 && result.size == 1) + ) { + "For a given method, You can either have a single REPLACE hook or BEFORE/AFTER hooks. Found:\n $result" + } + + return result + .filter { !isReplaceHookInJava6mode(it) } + .sortedByDescending { it.toString() } + } + + private fun isReplaceHookInJava6mode(hook: Hook): Boolean { + if (java6Mode && hook.hookType == HookType.REPLACE) { + if (showUnsupportedHookWarning.getAndSet(false)) { + println( + """WARN: Some hooks could not be applied to class files built for Java 7 or lower. + |WARN: Ensure that the fuzz target and its dependencies are compiled with + |WARN: -target 8 or higher to identify as many bugs as possible. + """.trimMargin() + ) + } + return true + } + return false } // Stores all arguments for a method call in a local object array. @@ -350,7 +363,7 @@ private class HookMethodVisitor( } // Removes a primitive value from the top of the operand stack - // and pushes it enclosed in it's wrapper type (e.g. removes int, pushes Integer). + // and pushes it enclosed in its wrapper type (e.g. removes int, pushes Integer). // This is done by calling .valueOf(...) on the wrapper class. private fun wrapTypeIfPrimitive(unwrappedTypeDescriptor: String) { if (!isPrimitiveType(unwrappedTypeDescriptor) || unwrappedTypeDescriptor == "V") return diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt new file mode 100644 index 00000000..66a21ee7 --- /dev/null +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt @@ -0,0 +1,114 @@ +// Copyright 2021 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.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import com.code_intelligence.jazzer.utils.ClassNameGlobber +import com.code_intelligence.jazzer.utils.descriptor +import io.github.classgraph.ClassGraph +import io.github.classgraph.ScanResult +import java.lang.reflect.Method + +data class Hooks( + val hooks: List<Hook>, + val hookClasses: Set<Class<*>>, + val additionalHookClassNameGlobber: ClassNameGlobber +) { + + companion object { + fun loadHooks(vararg hookClassNames: Set<String>): List<Hooks> { + return ClassGraph() + .enableClassInfo() + .enableSystemJarsAndModules() + .rejectPackages("jaz.*", "com.code_intelligence.jazzer.*") + .scan() + .use { scanResult -> + // Capture scanResult in HooksLoader field to not pass it through + // all internal hook loading methods. + val loader = HooksLoader(scanResult) + hookClassNames.map(loader::load) + } + } + + private class HooksLoader(private val scanResult: ScanResult) { + fun load(hookClassNames: Set<String>): Hooks { + val hooksWithHookClasses = hookClassNames.flatMap(::loadHooks) + val hooks = hooksWithHookClasses.map { it.first } + val hookClasses = hooksWithHookClasses.map { it.second }.toSet() + val additionalHookClassNameGlobber = ClassNameGlobber( + hooks.flatMap(Hook::additionalClassesToHook), + emptyList() + ) + return Hooks(hooks, hookClasses, additionalHookClassNameGlobber) + } + + private fun loadHooks(hookClassName: String): List<Pair<Hook, Class<*>>> { + return try { + // Custom hook classes outside the agent jar can not be found by bootstrap + // class loader, so use the system class loader as that will be the main application + // class loader and can access jars on the classpath. + // We let the static initializers of hook classes execute so that hooks can run + // code before the fuzz target class has been loaded (e.g., register themselves + // for the onFuzzTargetReady callback). + val hookClass = Class.forName(hookClassName, true, ClassLoader.getSystemClassLoader()) + loadHooks(hookClass).also { + println("INFO: Loaded ${it.size} hooks from $hookClassName") + }.map { + it to hookClass + } + } catch (e: ClassNotFoundException) { + println("WARN: Failed to load hooks from $hookClassName: ${e.printStackTrace()}") + emptyList() + } + } + + private fun loadHooks(hookClass: Class<*>): List<Hook> { + val hooks = mutableListOf<Hook>() + for (method in hookClass.methods.sortedBy { it.descriptor }) { + method.getAnnotation(MethodHook::class.java)?.let { + hooks.addAll(verifyAndGetHooks(method, it)) + } + method.getAnnotation(MethodHooks::class.java)?.let { + it.value.forEach { hookAnnotation -> + hooks.addAll(verifyAndGetHooks(method, hookAnnotation)) + } + } + } + return hooks + } + + private fun verifyAndGetHooks(hookMethod: Method, hookData: MethodHook): List<Hook> { + return lookupClassesToHook(hookData.targetClassName) + .map { className -> + Hook.createAndVerifyHook(hookMethod, hookData, className) + } + } + + private fun lookupClassesToHook(annotationTargetClassName: String): List<String> { + // Allowing arbitrary exterior whitespace in the target class name allows for an easy workaround + // for mangled hooks due to shading applied to hooks. + val targetClassName = annotationTargetClassName.trim() + val targetClassInfo = scanResult.getClassInfo(targetClassName) ?: return listOf(targetClassName) + val additionalTargetClasses = when { + targetClassInfo.isInterface -> scanResult.getClassesImplementing(targetClassName) + targetClassInfo.isAbstract -> scanResult.getSubclasses(targetClassName) + else -> emptyList() + } + return (listOf(targetClassName) + additionalTargetClasses.map { it.name }).sorted() + } + } + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt index 86ad45a3..78793842 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt @@ -14,8 +14,8 @@ package com.code_intelligence.jazzer.instrumentor -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodNode +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.MethodNode enum class InstrumentationType { CMP, diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java new file mode 100644 index 00000000..0512ec2a --- /dev/null +++ b/agent/src/main/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 com.code_intelligence.jazzer.third_party.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; + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt index e6d3176e..65f11e52 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt @@ -15,19 +15,19 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.AbstractInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.ClassNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.InsnList -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.InsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.IntInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.LdcInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.LookupSwitchInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.TableSwitchInsnNode +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.IntInsnNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.LookupSwitchInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.TableSwitchInsnNode internal class TraceDataFlowInstrumentor(private val types: Set<InstrumentationType>, callbackClass: Class<*> = TraceDataFlowNativeCallbacks::class.java) : Instrumentor { @@ -133,7 +133,7 @@ internal class TraceDataFlowInstrumentor(private val types: Set<InstrumentationT } private fun InsnList.pushFakePc() { - add(LdcInsnNode(random.nextInt(4096))) + add(LdcInsnNode(random.nextInt(512))) } private fun longCmpInstrumentation() = InsnList().apply { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules deleted file mode 100644 index c2092b3b..00000000 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules +++ /dev/null @@ -1 +0,0 @@ -rule org.** com.code_intelligence.jazzer.third_party.@1
\ No newline at end of file diff --git a/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel index df28adb4..08bd7653 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel @@ -3,8 +3,7 @@ load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") java_jni_library( name = "replay", srcs = ["Replayer.java"], - native_libs = ["//agent/src/main/native/com/code_intelligence/jazzer/replay"], - visibility = ["//agent/src/main/native/com/code_intelligence/jazzer/replay:__pkg__"], + native_libs = ["//driver/src/main/native/com/code_intelligence/jazzer/driver:fuzzed_data_provider_standalone"], deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/api", "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider", diff --git a/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java b/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java index fc6bfc4f..0a250d1a 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java @@ -29,8 +29,10 @@ public class Replayer { public static final int STATUS_OTHER_ERROR = 1; static { + System.setProperty("jazzer.is_replayer", "true"); try { - RulesJni.loadLibrary("replay", Replayer.class); + RulesJni.loadLibrary( + "fuzzed_data_provider_standalone", "/com/code_intelligence/jazzer/driver"); } catch (Throwable t) { t.printStackTrace(); System.exit(STATUS_OTHER_ERROR); @@ -104,7 +106,9 @@ public class Replayer { try { Method fuzzerTestOneInput = fuzzTarget.getMethod("fuzzerTestOneInput", FuzzedDataProvider.class); - fuzzerTestOneInput.invoke(null, makeFuzzedDataProvider(input)); + try (FuzzedDataProviderImpl fuzzedDataProvider = FuzzedDataProviderImpl.withJavaData(input)) { + fuzzerTestOneInput.invoke(null, fuzzedDataProvider); + } return; } catch (Exception e) { handleInvokeException(e, fuzzTarget); @@ -149,11 +153,4 @@ public class Replayer { } } } - - private static FuzzedDataProvider makeFuzzedDataProvider(byte[] input) { - feedFuzzedDataProvider(input); - return new FuzzedDataProviderImpl(); - } - - private static native void feedFuzzedDataProvider(byte[] input); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel index 095b0bf8..0d8162d5 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -1,47 +1,87 @@ -load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") -java_library( +java_jni_library( name = "fuzzed_data_provider", srcs = [ "FuzzedDataProviderImpl.java", ], - visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__"], + visibility = [ + "//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__", + "//agent/src/test/java/com/code_intelligence/jazzer/runtime:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], deps = [ + ":unsafe_provider", "//agent/src/main/java/com/code_intelligence/jazzer/api", ], ) -java_library( +java_jni_library( + name = "coverage_map", + srcs = ["CoverageMap.java"], + visibility = [ + "//agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/test:__subpackages__", + ], + deps = [ + ":unsafe_provider", + ], +) + +java_jni_library( name = "signal_handler", srcs = ["SignalHandler.java"], - javacopts = [ - "-XDenableSunApiLintControl", + native_libs = ["//agent/src/main/native/com/code_intelligence/jazzer/runtime:jazzer_signal_handler"], + visibility = [ + "//agent/src/main/native/com/code_intelligence/jazzer/runtime:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", ], ) -kt_jvm_library( +java_jni_library( + name = "trace_data_flow_native_callbacks", + srcs = ["TraceDataFlowNativeCallbacks.java"], + visibility = [ + "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/utils", + ], +) + +java_library( + name = "unsafe_provider", + srcs = ["UnsafeProvider.java"], + visibility = [ + "//driver/src:__subpackages__", + "//sanitizers/src/main/java:__subpackages__", + ], +) + +java_library( name = "runtime", srcs = [ - "CoverageMap.java", - "ExceptionUtils.kt", "HardToCatchError.java", "JazzerInternal.java", - "ManifestUtils.kt", "NativeLibHooks.java", "RecordingFuzzedDataProvider.java", - "SignalHandler.java", "TraceCmpHooks.java", - "TraceDataFlowNativeCallbacks.java", "TraceDivHooks.java", "TraceIndirHooks.java", ], visibility = ["//visibility:public"], runtime_deps = [ + ":signal_handler", "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz", ], deps = [ + ":coverage_map", ":fuzzed_data_provider", - ":signal_handler", + ":trace_data_flow_native_callbacks", "//agent/src/main/java/com/code_intelligence/jazzer/api", "//agent/src/main/java/com/code_intelligence/jazzer/utils", ], diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java index af2424a2..4069d25a 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java @@ -14,20 +14,116 @@ package com.code_intelligence.jazzer.runtime; -import java.nio.ByteBuffer; +import com.github.fmeum.rules_jni.RulesJni; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import sun.misc.Unsafe; /** - * Represents the Java view on a libFuzzer 8 bit counter coverage map. - * By using a direct ByteBuffer, the counter array is shared directly with - * native code. + * Represents the Java view on a libFuzzer 8 bit counter coverage map. By using a direct ByteBuffer, + * the counters are shared directly with native code. */ final public class CoverageMap { - public static ByteBuffer mem = ByteBuffer.allocateDirect(0); + static { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + + private static final String ENV_MAX_NUM_COUNTERS = "JAZZER_MAX_NUM_COUNTERS"; + + private static final int MAX_NUM_COUNTERS = System.getenv(ENV_MAX_NUM_COUNTERS) != null + ? Integer.parseInt(System.getenv(ENV_MAX_NUM_COUNTERS)) + : 1 << 20; - public static void enlargeCoverageMap() { - registerNewCoverageCounters(); - System.out.println("INFO: New number of inline 8-bit counters: " + mem.capacity()); + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + + static { + if (UNSAFE == null) { + System.out.println("ERROR: Failed to get Unsafe instance for CoverageMap.%n" + + " Please file a bug at:%n" + + " https://github.com/CodeIntelligenceTesting/jazzer/issues/new"); + System.exit(1); + } } - private static native void registerNewCoverageCounters(); + /** + * The collection of coverage counters directly interacted with by classes that are instrumented + * for coverage. The instrumentation assumes that this is always one contiguous block of memory, + * so it is allocated once at maximum size. Using a larger number here increases the memory usage + * of all fuzz targets, but has otherwise no impact on performance. + */ + public static final long countersAddress = UNSAFE.allocateMemory(MAX_NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, MAX_NUM_COUNTERS, (byte) 0); + initialize(countersAddress); + } + + private static final int INITIAL_NUM_COUNTERS = 1 << 9; + + static { + registerNewCounters(0, INITIAL_NUM_COUNTERS); + } + + /** + * The number of coverage counters that are currently registered with libFuzzer. This number grows + * dynamically as classes are instrumented and should be kept as low as possible as libFuzzer has + * to iterate over the whole map for every execution. + */ + private static int currentNumCounters = INITIAL_NUM_COUNTERS; + + // Called via reflection. + @SuppressWarnings("unused") + public static void enlargeIfNeeded(int nextId) { + int newNumCounters = currentNumCounters; + while (nextId >= newNumCounters) { + newNumCounters = 2 * newNumCounters; + if (newNumCounters > MAX_NUM_COUNTERS) { + System.out.printf("ERROR: Maximum number (%s) of coverage counters exceeded. Try to%n" + + " limit the scope of a single fuzz target as much as possible to keep the%n" + + " fuzzer fast.%n" + + " If that is not possible, the maximum number of counters can be increased%n" + + " via the %s environment variable.", + MAX_NUM_COUNTERS, ENV_MAX_NUM_COUNTERS); + System.exit(1); + } + } + if (newNumCounters > currentNumCounters) { + registerNewCounters(currentNumCounters, newNumCounters); + currentNumCounters = newNumCounters; + System.out.println("INFO: New number of coverage counters: " + currentNumCounters); + } + } + + // Called by the coverage instrumentation. + @SuppressWarnings("unused") + 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)); + } + + public static Set<Integer> getCoveredIds() { + Set<Integer> coveredIds = new HashSet<>(); + for (int id = 0; id < currentNumCounters; id++) { + if (UNSAFE.getByte(countersAddress + id) > 0) { + coveredIds.add(id); + } + } + return Collections.unmodifiableSet(coveredIds); + } + + public static void replayCoveredIds(Set<Integer> coveredIds) { + for (int id : coveredIds) { + UNSAFE.putByte(countersAddress + id, (byte) 1); + } + } + + // Returns the IDs of all blocks that have been covered in at least one run (not just the current + // one). + public static native int[] getEverCoveredIds(); + + private static native void initialize(long countersAddress); + + private static native void registerNewCounters(int oldNumCounters, int newNumCounters); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java index fe4d8ac7..b7aad33e 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java @@ -15,9 +15,119 @@ package com.code_intelligence.jazzer.runtime; import com.code_intelligence.jazzer.api.FuzzedDataProvider; - -public class FuzzedDataProviderImpl implements FuzzedDataProvider { - public FuzzedDataProviderImpl() {} +import com.github.fmeum.rules_jni.RulesJni; +import sun.misc.Unsafe; + +public class FuzzedDataProviderImpl implements FuzzedDataProvider, AutoCloseable { + static { + // The replayer loads a standalone version of the FuzzedDataProvider. + if (System.getProperty("jazzer.is_replayer") == null) { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + nativeInit(); + } + + private static native void nativeInit(); + + private final boolean ownsNativeData; + private long originalDataPtr; + private int originalRemainingBytes; + + // Accessed in fuzzed_data_provider.cpp. + private long dataPtr; + private int remainingBytes; + + private FuzzedDataProviderImpl(long dataPtr, int remainingBytes, boolean ownsNativeData) { + this.ownsNativeData = ownsNativeData; + this.originalDataPtr = dataPtr; + this.dataPtr = dataPtr; + this.originalRemainingBytes = remainingBytes; + this.remainingBytes = remainingBytes; + } + + /** + * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array. + * + * <ul> + * <li>{@link #close()} <b>must</b> be called on instances created with this method to free the + * native copy of the Java + * {@code byte} array. + * <li>{@link #setNativeData(long, int)} <b>must not</b> be called on instances created with this + * method. + * + * @param data the raw bytes used as input + * @return a {@link FuzzedDataProvider} backed by {@code data} + */ + public static FuzzedDataProviderImpl withJavaData(byte[] data) { + return new FuzzedDataProviderImpl(allocateNativeCopy(data), data.length, true); + } + + /** + * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array. + * + * <p>The backing array can be set at any time using {@link #setNativeData(long, int)} and is + * initially empty. + * + * @return a {@link FuzzedDataProvider} backed by an empty array. + */ + public static FuzzedDataProviderImpl withNativeData() { + return new FuzzedDataProviderImpl(0, 0, false); + } + + /** + * Replaces the current native backing array. + * + * <p><b>Must not</b> be called on instances created with {@link #withJavaData(byte[])}. + * + * @param dataPtr a native pointer to the new backing array + * @param dataLength the length of the new backing array + */ + public void setNativeData(long dataPtr, int dataLength) { + this.originalDataPtr = dataPtr; + this.dataPtr = dataPtr; + this.originalRemainingBytes = dataLength; + this.remainingBytes = dataLength; + } + + /** + * Resets the FuzzedDataProvider state to read from the beginning to the end of its current + * backing item. + */ + public void reset() { + dataPtr = originalDataPtr; + remainingBytes = originalRemainingBytes; + } + + /** + * Releases native memory allocated for this instance (if any). + * + * <p>While the instance should not be used after this method returns, no usage of {@link + * FuzzedDataProvider} methods can result in memory corruption. + */ + @Override + public void close() { + if (originalDataPtr == 0) { + return; + } + if (ownsNativeData) { + UNSAFE.freeMemory(originalDataPtr); + } + // Prevent double-frees and use-after-frees by effectively making all methods no-ops after + // close() has been called. + originalDataPtr = 0; + originalRemainingBytes = 0; + dataPtr = 0; + remainingBytes = 0; + } + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + + private static long allocateNativeCopy(byte[] data) { + long nativeCopy = UNSAFE.allocateMemory(data.length); + UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, nativeCopy, data.length); + return nativeCopy; + } @Override public native boolean consumeBoolean(); @@ -25,23 +135,51 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native byte consumeByte(); - @Override public native byte consumeByte(byte min, byte max); + @Override + public byte consumeByte(byte min, byte max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeByteUnchecked(min, max); + } @Override public native short consumeShort(); - @Override public native short consumeShort(short min, short max); + @Override + public short consumeShort(short min, short max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeShortUnchecked(min, max); + } @Override public native short[] consumeShorts(int maxLength); @Override public native int consumeInt(); - @Override public native int consumeInt(int min, int max); + @Override + public int consumeInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeIntUnchecked(min, max); + } @Override public native int[] consumeInts(int maxLength); @Override public native long consumeLong(); - @Override public native long consumeLong(long min, long max); + @Override + public long consumeLong(long min, long max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeLongUnchecked(min, max); + } @Override public native long[] consumeLongs(int maxLength); @@ -49,13 +187,27 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native float consumeRegularFloat(); - @Override public native float consumeRegularFloat(float min, float max); + @Override + public float consumeRegularFloat(float min, float max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %f, max: %f)", min, max)); + } + return consumeRegularFloatUnchecked(min, max); + } @Override public native float consumeProbabilityFloat(); @Override public native double consumeDouble(); - @Override public native double consumeRegularDouble(double min, double max); + @Override + public double consumeRegularDouble(double min, double max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %f, max: %f)", min, max)); + } + return consumeRegularDoubleUnchecked(min, max); + } @Override public native double consumeRegularDouble(); @@ -63,7 +215,14 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native char consumeChar(); - @Override public native char consumeChar(char min, char max); + @Override + public char consumeChar(char min, char max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %c, max: %c)", min, max)); + } + return consumeCharUnchecked(min, max); + } @Override public native char consumeCharNoSurrogates(); @@ -80,4 +239,12 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native byte[] consumeRemainingAsBytes(); @Override public native int remainingBytes(); + + private native byte consumeByteUnchecked(byte min, byte max); + private native short consumeShortUnchecked(short min, short max); + private native char consumeCharUnchecked(char min, char max); + private native int consumeIntUnchecked(int min, int max); + private native long consumeLongUnchecked(long min, long max); + private native float consumeRegularFloatUnchecked(float min, float max); + private native double consumeRegularDoubleUnchecked(double min, double max); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java index 8bc1b38c..79c851ad 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java @@ -14,9 +14,12 @@ package com.code_intelligence.jazzer.runtime; +import java.util.ArrayList; + final public class JazzerInternal { - // Accessed from native code. - private static Throwable lastFinding; + private static final ArrayList<Runnable> ON_FUZZ_TARGET_READY_CALLBACKS = new ArrayList<>(); + + public static Throwable lastFinding; // Accessed from api.Jazzer via reflection. public static void reportFindingFromHook(Throwable finding) { @@ -26,4 +29,13 @@ final public class JazzerInternal { // target returns even if this Error is swallowed. throw new HardToCatchError(); } + + public static void registerOnFuzzTargetReadyCallback(Runnable callback) { + ON_FUZZ_TARGET_READY_CALLBACKS.add(callback); + } + + public static void onFuzzTargetReady(String fuzzTargetClass) { + ON_FUZZ_TARGET_READY_CALLBACKS.forEach(Runnable::run); + ON_FUZZ_TARGET_READY_CALLBACKS.clear(); + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java index 976e024c..4eb80222 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java @@ -18,49 +18,33 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Base64; // Wraps the native FuzzedDataProviderImpl and serializes all its return values // into a Base64-encoded string. -final class RecordingFuzzedDataProvider implements InvocationHandler { - private final FuzzedDataProvider target = new FuzzedDataProviderImpl(); +public final class RecordingFuzzedDataProvider implements FuzzedDataProvider { + private final FuzzedDataProvider target; private final ArrayList<Object> recordedReplies = new ArrayList<>(); - private RecordingFuzzedDataProvider() {} + private RecordingFuzzedDataProvider(FuzzedDataProvider target) { + this.target = target; + } - // Called from native code. - public static FuzzedDataProvider makeFuzzedDataProviderProxy() { - return (FuzzedDataProvider) Proxy.newProxyInstance( - RecordingFuzzedDataProvider.class.getClassLoader(), new Class[] {FuzzedDataProvider.class}, - new RecordingFuzzedDataProvider()); + public static FuzzedDataProvider makeFuzzedDataProviderProxy(FuzzedDataProvider target) { + return new RecordingFuzzedDataProvider(target); } - // Called from native code. public static String serializeFuzzedDataProviderProxy(FuzzedDataProvider proxy) throws IOException { - return ((RecordingFuzzedDataProvider) Proxy.getInvocationHandler(proxy)).serialize(); + return ((RecordingFuzzedDataProvider) proxy).serialize(); } - private Object recordAndReturn(Object object) { + private <T> T recordAndReturn(T object) { recordedReplies.add(object); return object; } - @Override - public Object invoke(Object object, Method method, Object[] args) throws Throwable { - if (method.isDefault()) { - // Default methods in FuzzedDataProvider are implemented in Java and - // don't need to be recorded. - return method.invoke(target, args); - } else { - return recordAndReturn(method.invoke(target, args)); - } - } - private String serialize() throws IOException { byte[] rawOut; try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) { @@ -71,4 +55,159 @@ final class RecordingFuzzedDataProvider implements InvocationHandler { } return Base64.getEncoder().encodeToString(rawOut); } + + @Override + public boolean consumeBoolean() { + return recordAndReturn(target.consumeBoolean()); + } + + @Override + public boolean[] consumeBooleans(int maxLength) { + return recordAndReturn(target.consumeBooleans(maxLength)); + } + + @Override + public byte consumeByte() { + return recordAndReturn(target.consumeByte()); + } + + @Override + public byte consumeByte(byte min, byte max) { + return recordAndReturn(target.consumeByte(min, max)); + } + + @Override + public byte[] consumeBytes(int maxLength) { + return recordAndReturn(target.consumeBytes(maxLength)); + } + + @Override + public byte[] consumeRemainingAsBytes() { + return recordAndReturn(target.consumeRemainingAsBytes()); + } + + @Override + public short consumeShort() { + return recordAndReturn(target.consumeShort()); + } + + @Override + public short consumeShort(short min, short max) { + return recordAndReturn(target.consumeShort(min, max)); + } + + @Override + public short[] consumeShorts(int maxLength) { + return recordAndReturn(target.consumeShorts(maxLength)); + } + + @Override + public int consumeInt() { + return recordAndReturn(target.consumeInt()); + } + + @Override + public int consumeInt(int min, int max) { + return recordAndReturn(target.consumeInt(min, max)); + } + + @Override + public int[] consumeInts(int maxLength) { + return recordAndReturn(target.consumeInts(maxLength)); + } + + @Override + public long consumeLong() { + return recordAndReturn(target.consumeLong()); + } + + @Override + public long consumeLong(long min, long max) { + return recordAndReturn(target.consumeLong(min, max)); + } + + @Override + public long[] consumeLongs(int maxLength) { + return recordAndReturn(target.consumeLongs(maxLength)); + } + + @Override + public float consumeFloat() { + return recordAndReturn(target.consumeFloat()); + } + + @Override + public float consumeRegularFloat() { + return recordAndReturn(target.consumeRegularFloat()); + } + + @Override + public float consumeRegularFloat(float min, float max) { + return recordAndReturn(target.consumeRegularFloat(min, max)); + } + + @Override + public float consumeProbabilityFloat() { + return recordAndReturn(target.consumeProbabilityFloat()); + } + + @Override + public double consumeDouble() { + return recordAndReturn(target.consumeDouble()); + } + + @Override + public double consumeRegularDouble() { + return recordAndReturn(target.consumeRegularDouble()); + } + + @Override + public double consumeRegularDouble(double min, double max) { + return recordAndReturn(target.consumeRegularDouble(min, max)); + } + + @Override + public double consumeProbabilityDouble() { + return recordAndReturn(target.consumeProbabilityDouble()); + } + + @Override + public char consumeChar() { + return recordAndReturn(target.consumeChar()); + } + + @Override + public char consumeChar(char min, char max) { + return recordAndReturn(target.consumeChar(min, max)); + } + + @Override + public char consumeCharNoSurrogates() { + return recordAndReturn(target.consumeCharNoSurrogates()); + } + + @Override + public String consumeString(int maxLength) { + return recordAndReturn(target.consumeString(maxLength)); + } + + @Override + public String consumeRemainingAsString() { + return recordAndReturn(target.consumeRemainingAsString()); + } + + @Override + public String consumeAsciiString(int maxLength) { + return recordAndReturn(target.consumeAsciiString(maxLength)); + } + + @Override + public String consumeRemainingAsAsciiString() { + return recordAndReturn(target.consumeRemainingAsAsciiString()); + } + + @Override + public int remainingBytes() { + return recordAndReturn(target.remainingBytes()); + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java index 0a42aa94..49ee80c8 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java @@ -14,13 +14,18 @@ package com.code_intelligence.jazzer.runtime; +import com.github.fmeum.rules_jni.RulesJni; import sun.misc.Signal; -@SuppressWarnings({"unused", "sunapi"}) -final class SignalHandler { - public static native void handleInterrupt(); - - public static void setupSignalHandlers() { +public final class SignalHandler { + static { + RulesJni.loadLibrary("jazzer_signal_handler", SignalHandler.class); Signal.handle(new Signal("INT"), sig -> handleInterrupt()); } + + public static void initialize() { + // Implicitly runs the static initializer. + } + + private static native void handleInterrupt(); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java index 352da8ea..37e8eaeb 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java @@ -18,6 +18,7 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.MethodHook; import java.lang.invoke.MethodHandle; import java.util.Arrays; +import java.util.ConcurrentModificationException; import java.util.Map; import java.util.TreeMap; @@ -80,6 +81,18 @@ final public class TraceCmpHooks { } } + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Object", targetMethod = "equals") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.CharSequence", targetMethod = "equals") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Number", targetMethod = "equals") + public static void + genericEquals( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (!returnValue && arguments[0] != null && thisObject.getClass() == arguments[0].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(thisObject, arguments[0], hookId); + } + } + @MethodHook( type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "compareTo") @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", @@ -193,9 +206,9 @@ final public class TraceCmpHooks { replace( MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) { String original = (String) thisObject; - String target = arguments[0].toString(); // Report only if the replacement was not successful. if (original.equals(returnValue)) { + String target = arguments[0].toString(); TraceDataFlowNativeCallbacks.traceStrstr(original, target, hookId); } } @@ -205,11 +218,11 @@ final public class TraceCmpHooks { public static void arraysEquals( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (returnValue) + return; byte[] first = (byte[]) arguments[0]; byte[] second = (byte[]) arguments[1]; - if (!returnValue) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); } @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals", @@ -217,13 +230,13 @@ final public class TraceCmpHooks { public static void arraysEqualsRange( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (returnValue) + return; byte[] first = Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]); byte[] second = Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]); - if (!returnValue) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); } @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", @@ -233,11 +246,11 @@ final public class TraceCmpHooks { public static void arraysCompare( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == 0) + return; byte[] first = (byte[]) arguments[0]; byte[] second = (byte[]) arguments[1]; - if (returnValue != 0) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); } @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", @@ -247,34 +260,22 @@ final public class TraceCmpHooks { public static void arraysCompareRange( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == 0) + return; byte[] first = Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]); byte[] second = Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]); - if (returnValue != 0) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); } // The maximal number of elements of a non-TreeMap Map that will be sorted and searched for the // key closest to the current lookup key in the mapGet hook. private static final int MAX_NUM_KEYS_TO_ENUMERATE = 100; - @MethodHook(type = HookType.AFTER, targetClassName = "com.google.common.collect.ImmutableMap", - targetMethod = "get") - @MethodHook( - type = HookType.AFTER, targetClassName = "java.util.AbstractMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.EnumMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.HashMap", targetMethod = "get") - @MethodHook( - type = HookType.AFTER, targetClassName = "java.util.LinkedHashMap", targetMethod = "get") + @SuppressWarnings({"rawtypes", "unchecked"}) @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Map", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.SortedMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.TreeMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.concurrent.ConcurrentMap", - targetMethod = "get") - public static void - mapGet( + public static void mapGet( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { if (returnValue != null) return; @@ -291,31 +292,47 @@ final public class TraceCmpHooks { // https://github.com/llvm/llvm-project/blob/318942de229beb3b2587df09e776a50327b5cef0/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L564 Object lowerBoundKey = null; Object upperBoundKey = null; - if (map instanceof TreeMap) { - final TreeMap treeMap = (TreeMap) map; - lowerBoundKey = treeMap.floorKey(currentKey); - upperBoundKey = treeMap.ceilingKey(currentKey); - } else if (currentKey instanceof Comparable) { - final Comparable comparableKey = (Comparable) currentKey; - // Find two keys that bracket currentKey. - // Note: This is not deterministic if map.size() > MAX_NUM_KEYS_TO_ENUMERATE. - int enumeratedKeys = 0; - for (Object validKey : map.keySet()) { - if (validKey == null) - continue; - // If the key sorts lower than the non-existing key, but higher than the current lower - // bound, update the lower bound and vice versa for the upper bound. - if (comparableKey.compareTo(validKey) > 0 - && (lowerBoundKey == null || ((Comparable) validKey).compareTo(lowerBoundKey) > 0)) { - lowerBoundKey = validKey; + try { + if (map instanceof TreeMap) { + final TreeMap treeMap = (TreeMap) map; + try { + lowerBoundKey = treeMap.floorKey(currentKey); + upperBoundKey = treeMap.ceilingKey(currentKey); + } catch (ClassCastException ignored) { + // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be + // compared to the maps keys. } - if (comparableKey.compareTo(validKey) < 0 - && (upperBoundKey == null || ((Comparable) validKey).compareTo(upperBoundKey) < 0)) { - upperBoundKey = validKey; + } else if (currentKey instanceof Comparable) { + final Comparable comparableCurrentKey = (Comparable) currentKey; + // Find two keys that bracket currentKey. + // Note: This is not deterministic if map.size() > MAX_NUM_KEYS_TO_ENUMERATE. + int enumeratedKeys = 0; + for (Object validKey : map.keySet()) { + if (!(validKey instanceof Comparable)) + continue; + final Comparable comparableValidKey = (Comparable) validKey; + // If the key sorts lower than the non-existing key, but higher than the current lower + // bound, update the lower bound and vice versa for the upper bound. + try { + if (comparableValidKey.compareTo(comparableCurrentKey) < 0 + && (lowerBoundKey == null || comparableValidKey.compareTo(lowerBoundKey) > 0)) { + lowerBoundKey = validKey; + } + if (comparableValidKey.compareTo(comparableCurrentKey) > 0 + && (upperBoundKey == null || comparableValidKey.compareTo(upperBoundKey) < 0)) { + upperBoundKey = validKey; + } + } catch (ClassCastException ignored) { + // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be + // compared to the maps keys. + } + if (enumeratedKeys++ > MAX_NUM_KEYS_TO_ENUMERATE) + break; } - if (enumeratedKeys++ > MAX_NUM_KEYS_TO_ENUMERATE) - break; } + } catch (ConcurrentModificationException ignored) { + // map was modified by another thread, skip this invocation + return; } // Modify the hook ID so that compares against distinct valid keys are traced separately. if (lowerBoundKey != null) { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java index 456d0cb9..821ade0d 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java @@ -15,49 +15,32 @@ package com.code_intelligence.jazzer.runtime; import com.code_intelligence.jazzer.utils.Utils; +import com.github.fmeum.rules_jni.RulesJni; import java.lang.reflect.Executable; +import java.nio.charset.Charset; @SuppressWarnings("unused") final public class TraceDataFlowNativeCallbacks { - /* trace-cmp */ - // Calls: void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2); - public static native void traceCmpInt(int arg1, int arg2, int pc); - - // Calls: void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2); - public static native void traceConstCmpInt(int arg1, int arg2, int pc); - - // Calls: void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2); - public static native void traceCmpLong(long arg1, long arg2, int pc); + static { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } - // Calls: void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases); - public static native void traceSwitch(long val, long[] cases, int pc); + // 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. + private static final Charset FUZZED_DATA_CHARSET = Charset.forName("CESU8"); - // Calls: void __sanitizer_weak_hook_memcmp(void *caller_pc, const void *b1, const void *b2, - // size_t n, int result); public static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); - // Calls: void __sanitizer_weak_hook_strcmp(void *called_pc, const char *s1, const char *s2, int - // result); - public static native void traceStrcmp(String s1, String s2, int result, int pc); - - // Calls: void __sanitizer_weak_hook_strstr(void *called_pc, const char *s1, const char *s2, char - // *result); - public static native void traceStrstr(String s1, String s2, int pc); - - /* trace-div */ - // Calls: void __sanitizer_cov_trace_div4(uint32_t Val); - public static native void traceDivInt(int val, int pc); - - // Calls: void __sanitizer_cov_trace_div8(uint64_t Val); - public static native void traceDivLong(long val, int pc); - - /* trace-gep */ - // Calls: void __sanitizer_cov_trace_gep(uintptr_t Idx); - public static native void traceGep(long val, int pc); + public static void traceStrcmp(String s1, String s2, int result, int pc) { + traceMemcmp(encodeForLibFuzzer(s1), encodeForLibFuzzer(s2), result, pc); + } - /* indirect-calls */ - // Calls: void __sanitizer_cov_trace_pc_indir(uintptr_t Callee); - private static native void tracePcIndir(int callee, int caller); + public static void traceStrstr(String s1, String s2, int pc) { + traceStrstr0(encodeForLibFuzzer(s2), pc); + } public static void traceReflectiveCall(Executable callee, int pc) { String className = callee.getDeclaringClass().getCanonicalName(); @@ -75,17 +58,45 @@ final public class TraceDataFlowNativeCallbacks { // The caller has to ensure that arg1 and arg2 have the same class. public static void traceGenericCmp(Object arg1, Object arg2, int pc) { - if (arg1 instanceof String) { - traceStrcmp((String) arg1, (String) arg2, 1, pc); - } else if (arg1 instanceof Integer || arg1 instanceof Short || arg1 instanceof Byte - || arg1 instanceof Character) { + if (arg1 instanceof CharSequence) { + traceStrcmp(arg1.toString(), arg2.toString(), 1, pc); + } else if (arg1 instanceof Integer) { traceCmpInt((int) arg1, (int) arg2, pc); } else if (arg1 instanceof Long) { traceCmpLong((long) arg1, (long) arg2, pc); + } else if (arg1 instanceof Short) { + traceCmpInt((short) arg1, (short) arg2, pc); + } else if (arg1 instanceof Byte) { + traceCmpInt((byte) arg1, (byte) arg2, pc); + } else if (arg1 instanceof Character) { + traceCmpInt((char) arg1, (char) arg2, pc); + } else if (arg1 instanceof Number) { + traceCmpLong(((Number) arg1).longValue(), ((Number) arg2).longValue(), pc); } else if (arg1 instanceof byte[]) { traceMemcmp((byte[]) arg1, (byte[]) arg2, 1, pc); } } + /* trace-cmp */ + public static native void traceCmpInt(int arg1, int arg2, int pc); + public static native void traceConstCmpInt(int arg1, int arg2, int pc); + public static native void traceCmpLong(long arg1, long arg2, int pc); + public static native void traceSwitch(long val, long[] cases, int pc); + /* trace-div */ + public static native void traceDivInt(int val, int pc); + public static native void traceDivLong(long val, int pc); + /* trace-gep */ + public static native void traceGep(long val, int pc); + /* indirect-calls */ + public static native void tracePcIndir(int callee, int caller); + public static native void handleLibraryLoad(); + + private static byte[] encodeForLibFuzzer(String str) { + // libFuzzer string hooks only ever consume the first 64 bytes, so we can definitely cut the + // string off after 64 characters. + return str.substring(0, Math.min(str.length(), 64)).getBytes(FUZZED_DATA_CHARSET); + } + + private static native void traceStrstr0(byte[] needle, int pc); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/UnsafeProvider.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/UnsafeProvider.java new file mode 100644 index 00000000..81f2a208 --- /dev/null +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/UnsafeProvider.java @@ -0,0 +1,50 @@ +// 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.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeProvider { + private static final Unsafe UNSAFE = getUnsafeInternal(); + + public static Unsafe getUnsafe() { + return UNSAFE; + } + + private static Unsafe getUnsafeInternal() { + try { + // The Java agent is loaded by the bootstrap class loader and should thus + // pass the security checks in getUnsafe. + return Unsafe.getUnsafe(); + } catch (Throwable unused) { + // If not running as an agent, use the classical reflection trick to get an Unsafe instance, + // taking into account that the private field may have a name other than "theUnsafe": + // https://android.googlesource.com/platform/libcore/+/gingerbread/luni/src/main/java/sun/misc/Unsafe.java#32 + try { + for (Field f : Unsafe.class.getDeclaredFields()) { + if (f.getType() == Unsafe.class) { + f.setAccessible(true); + return (Unsafe) f.get(null); + } + } + return null; + } catch (Throwable t) { + t.printStackTrace(); + return null; + } + } + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel index 5e301efc..10e3477c 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel @@ -4,7 +4,12 @@ kt_jvm_library( name = "utils", srcs = [ "ClassNameGlobber.kt", + "ExceptionUtils.kt", + "ManifestUtils.kt", "Utils.kt", ], visibility = ["//visibility:public"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + ], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt index 1f09afe3..44249c81 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt @@ -14,28 +14,41 @@ package com.code_intelligence.jazzer.utils -import java.lang.IllegalArgumentException - private val BASE_INCLUDED_CLASS_NAME_GLOBS = listOf( "**", // everything ) +// We use both a strong indicator for running as a Bazel test together with an indicator for a +// Bazel coverage run to rule out false positives. +private val IS_BAZEL_COVERAGE_RUN = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") != null && + System.getenv("COVERAGE_DIR") != null + +private val ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE = listOf( + "com.google.testing.coverage.**", + "org.jacoco.**", +) + private val BASE_EXCLUDED_CLASS_NAME_GLOBS = listOf( + // JDK internals "\\[**", // array types - "com.code_intelligence.jazzer.**", - "com.sun.**", // package for Proxy objects "java.**", "javax.**", - "jaz.Ter", // safe companion of the honeypot class used by sanitizers - "jaz.Zer", // honeypot class used by sanitizers "jdk.**", - "kotlin.**", "sun.**", -) + "com.sun.**", // package for Proxy objects + // Azul JDK internals + "com.azul.tooling.**", + // Kotlin internals + "kotlin.**", + // Jazzer internals + "com.code_intelligence.jazzer.**", + "jaz.Ter", // safe companion of the honeypot class used by sanitizers + "jaz.Zer", // honeypot class used by sanitizers +) + if (IS_BAZEL_COVERAGE_RUN) ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE else listOf() class ClassNameGlobber(includes: List<String>, excludes: List<String>) { // If no include globs are provided, start with all classes. - private val includeMatchers = (if (includes.isEmpty()) BASE_INCLUDED_CLASS_NAME_GLOBS else includes) + private val includeMatchers = includes.ifEmpty { BASE_INCLUDED_CLASS_NAME_GLOBS } .map(::SimpleGlobMatcher) // If no include globs are provided, additionally exclude stdlib classes as well as our own classes. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt index 31a61740..30f6fb30 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt @@ -14,7 +14,7 @@ @file:JvmName("ExceptionUtils") -package com.code_intelligence.jazzer.runtime +package com.code_intelligence.jazzer.utils import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow import java.lang.management.ManagementFactory @@ -163,4 +163,10 @@ fun dumpAllStackTraces() { } System.err.println() } + System.err.println("Garbage collector stats:") + System.err.println( + ManagementFactory.getGarbageCollectorMXBeans().joinToString("\n", "\n", "\n") { + "${it.name}: ${it.collectionCount} collections took ${it.collectionTime}ms" + } + ) } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt index d88c3e18..e7165e55 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.code_intelligence.jazzer.runtime +package com.code_intelligence.jazzer.utils import java.util.jar.Manifest object ManifestUtils { - const val FUZZ_TARGET_CLASS = "Jazzer-Fuzz-Target-Class" + private const val FUZZ_TARGET_CLASS = "Jazzer-Fuzz-Target-Class" const val HOOK_CLASSES = "Jazzer-Hook-Classes" fun combineManifestValues(attribute: String): List<String> { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt index af8cce9b..1b399baf 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt @@ -17,6 +17,8 @@ package com.code_intelligence.jazzer.utils import java.lang.reflect.Executable import java.lang.reflect.Method +import java.nio.ByteBuffer +import java.nio.channels.FileChannel val Class<*>.descriptor: String get() = when { @@ -80,3 +82,26 @@ fun simpleFastHash(vararg strings: String): Int { } return hash } + +/** + * Reads the [FileChannel] to the end as a UTF-8 string. + */ +fun FileChannel.readFully(): String { + check(size() <= Int.MAX_VALUE) + val buffer = ByteBuffer.allocate(size().toInt()) + while (buffer.hasRemaining()) { + when (read(buffer)) { + 0 -> throw IllegalStateException("No bytes read") + -1 -> break + } + } + return String(buffer.array()) +} + +/** + * Appends [string] to the end of the [FileChannel]. + */ +fun FileChannel.append(string: String) { + position(size()) + write(ByteBuffer.wrap(string.toByteArray())) +} diff --git a/agent/src/main/java/jaz/BUILD.bazel b/agent/src/main/java/jaz/BUILD.bazel new file mode 100644 index 00000000..c6cdcf13 --- /dev/null +++ b/agent/src/main/java/jaz/BUILD.bazel @@ -0,0 +1,8 @@ +filegroup( + name = "jaz", + srcs = [ + "Ter.java", + "Zer.java", + ], + visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/api:__pkg__"], +) diff --git a/agent/src/main/java/jaz/Ter.java b/agent/src/main/java/jaz/Ter.java new file mode 100644 index 00000000..7814396f --- /dev/null +++ b/agent/src/main/java/jaz/Ter.java @@ -0,0 +1,24 @@ +// Copyright 2021 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 jaz; + +/** + * A safe to use companion of {@link jaz.Zer} that is used to produce serializable instances of it + * with only light patching. + */ +@SuppressWarnings("unused") +public class Ter implements java.io.Serializable { + static final long serialVersionUID = 42L; +} diff --git a/agent/src/main/java/jaz/Zer.java b/agent/src/main/java/jaz/Zer.java new file mode 100644 index 00000000..08ca3d2e --- /dev/null +++ b/agent/src/main/java/jaz/Zer.java @@ -0,0 +1,234 @@ +// Copyright 2021 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 jaz; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; +import com.code_intelligence.jazzer.api.Jazzer; +import java.io.Closeable; +import java.io.Flushable; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.function.Function; + +/** + * A honeypot class that reports a finding on initialization. + * + * Class loading based on externally controlled data could lead to RCE + * depending on available classes on the classpath. Even if no applicable + * gadget class is available, allowing input to control class loading is a bad + * idea and should be prevented. A finding is generated whenever the class + * is loaded and initialized, regardless of its further use. + * <p> + * This class needs to implement {@link Serializable} to be considered in + * deserialization scenarios. It also implements common constructors, getter + * and setter and common interfaces to increase chances of passing + * deserialization checks. + * <p> + * <b>Note</b>: Jackson provides a nice list of "nasty classes" at + * <a + * href=https://github.com/FasterXML/jackson-databind/blob/2.14/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SubTypeValidator.java>SubTypeValidator</a>. + * <p> + * <b>Note</b>: This class must not be referenced in any way by the rest of the code, not even + * statically. When referring to it, always use its hardcoded class name {@code jaz.Zer}. + */ +@SuppressWarnings({"rawtypes", "unused"}) +public class Zer + implements Serializable, Cloneable, Comparable<Zer>, Comparator, Closeable, Flushable, Iterable, + Iterator, Runnable, Callable, Function, Collection, List { + static final long serialVersionUID = 42L; + + static { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh("Remote Code Execution\n" + + "Unrestricted class loading based on externally controlled data may allow\n" + + "remote code execution depending on available classes on the classpath.")); + } + + // Common constructors + + public Zer() {} + + public Zer(String arg1) {} + + public Zer(String arg1, Throwable arg2) {} + + // Getter/Setter + + public Object getJaz() { + return this; + } + + public void setJaz(String jaz) {} + + // Common interface stubs + + @Override + public void close() {} + + @Override + public void flush() {} + + @Override + public int compareTo(Zer o) { + return 0; + } + + @Override + public int compare(Object o1, Object o2) { + return 0; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean contains(Object o) { + return false; + } + + @Override + public Object[] toArray() { + return new Object[0]; + } + + @Override + public boolean add(Object o) { + return false; + } + + @Override + public boolean remove(Object o) { + return false; + } + + @Override + public boolean addAll(Collection c) { + return false; + } + + @Override + public boolean addAll(int index, Collection c) { + return false; + } + + @Override + public void clear() {} + + @Override + public Object get(int index) { + return this; + } + + @Override + public Object set(int index, Object element) { + return this; + } + + @Override + public void add(int index, Object element) {} + + @Override + public Object remove(int index) { + return this; + } + + @Override + public int indexOf(Object o) { + return 0; + } + + @Override + public int lastIndexOf(Object o) { + return 0; + } + + @Override + @SuppressWarnings("ConstantConditions") + public ListIterator listIterator() { + return null; + } + + @Override + @SuppressWarnings("ConstantConditions") + public ListIterator listIterator(int index) { + return null; + } + + @Override + public List subList(int fromIndex, int toIndex) { + return this; + } + + @Override + public boolean retainAll(Collection c) { + return false; + } + + @Override + public boolean removeAll(Collection c) { + return false; + } + + @Override + public boolean containsAll(Collection c) { + return false; + } + + @Override + public Object[] toArray(Object[] a) { + return new Object[0]; + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public void run() {} + + @Override + public boolean hasNext() { + return false; + } + + @Override + public Object next() { + return this; + } + + @Override + public Object call() throws Exception { + return this; + } + + @Override + public Object apply(Object o) { + return this; + } + + @Override + @SuppressWarnings("MethodDoesntCallSuperMethod") + public Object clone() { + return this; + } +} diff --git a/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel b/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel deleted file mode 100644 index 6b75fb8b..00000000 --- a/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") - -cc_jni_library( - name = "replay", - srcs = [ - "com_code_intelligence_jazzer_replay_Replayer.cpp", - ], - visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__"], - deps = [ - "//agent/src/main/java/com/code_intelligence/jazzer/replay:replay.hdrs", - "//driver:fuzzed_data_provider", - ], -) diff --git a/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp b/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp deleted file mode 100644 index c4bdfcfb..00000000 --- a/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2021 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. - -#include "com_code_intelligence_jazzer_replay_Replayer.h" - -#include <jni.h> - -#include "driver/fuzzed_data_provider.h" - -namespace { -uint8_t *data = nullptr; -} - -void Java_com_code_1intelligence_jazzer_replay_Replayer_feedFuzzedDataProvider( - JNIEnv *env, jclass, jbyteArray input) { - if (data == nullptr) { - jazzer::SetUpFuzzedDataProvider(*env); - } else { - delete[] data; - } - - std::size_t size = env->GetArrayLength(input); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->FatalError("Failed to get length of input"); - } - data = static_cast<uint8_t *>(operator new(size)); - if (data == nullptr) { - env->FatalError("Failed to allocate memory for a copy of the input"); - } - env->GetByteArrayRegion(input, 0, size, reinterpret_cast<jbyte *>(data)); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->FatalError("Failed to copy input"); - } - jazzer::FeedFuzzedDataProvider(data, size); -} diff --git a/agent/src/main/native/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/main/native/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..7d910474 --- /dev/null +++ b/agent/src/main/native/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,8 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") + +cc_jni_library( + name = "jazzer_signal_handler", + srcs = ["signal_handler.cpp"], + visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/runtime:__pkg__"], + deps = ["//agent/src/main/java/com/code_intelligence/jazzer/runtime:signal_handler.hdrs"], +) diff --git a/agent/src/main/native/com/code_intelligence/jazzer/runtime/signal_handler.cpp b/agent/src/main/native/com/code_intelligence/jazzer/runtime/signal_handler.cpp new file mode 100644 index 00000000..2600a53a --- /dev/null +++ b/agent/src/main/native/com/code_intelligence/jazzer/runtime/signal_handler.cpp @@ -0,0 +1,40 @@ +// Copyright 2021 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. + +#include <jni.h> + +#include <atomic> +#include <csignal> + +#include "com_code_intelligence_jazzer_runtime_SignalHandler.h" + +#ifdef _WIN32 +// Windows does not have SIGUSR1, which triggers a graceful exit of libFuzzer. +// Instead, trigger a hard exit. +#define SIGUSR1 SIGTERM +#endif + +// Handles SIGINT raised while running Java code. +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_SignalHandler_handleInterrupt( + JNIEnv *, jclass) { + static std::atomic<bool> already_exiting{false}; + if (!already_exiting.exchange(true)) { + // Let libFuzzer exit gracefully when the JVM received SIGINT. + raise(SIGUSR1); + } else { + // Exit libFuzzer forcefully on repeated SIGINTs. + raise(SIGTERM); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java index 66a85db6..59ef238d 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import java.util.Arrays; import java.util.Collections; +import org.junit.BeforeClass; import org.junit.Test; public class AutofuzzTest { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel index 9192ff77..f2537b73 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel +++ b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -16,6 +16,7 @@ java_test( ], deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", "@maven//:junit_junit", ], ) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java index 0615e9ae..0906d1d5 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java @@ -22,19 +22,13 @@ import com.code_intelligence.jazzer.api.CannedFuzzedDataProvider; import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.google.json.JsonSanitizer; import java.io.ByteArrayInputStream; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; +import java.util.Map; import org.junit.Test; public class MetaTest { - public static boolean isFive(int arg) { - return arg == 5; - } - - public static boolean intEquals(int arg1, int arg2) { - return arg1 == arg2; - } - public enum TestEnum { FOO, BAR, @@ -42,7 +36,7 @@ public class MetaTest { } @Test - public void testConsume() { + public void testConsume() throws NoSuchMethodException { consumeTestCase(5, "5", Collections.singletonList(5)); consumeTestCase((short) 5, "(short) 5", Collections.singletonList((short) 5)); consumeTestCase(5L, "5L", Collections.singletonList(5L)); @@ -121,6 +115,52 @@ public class MetaTest { consumeTestCase(YourAverageJavaClass.class, "com.code_intelligence.jazzer.autofuzz.YourAverageJavaClass.class", Collections.singletonList((byte) 1)); + + Type stringStringMapType = + MetaTest.class.getDeclaredMethod("returnsStringStringMap").getGenericReturnType(); + Map<String, String> expectedMap = + java.util.stream.Stream + .of(new java.util.AbstractMap.SimpleEntry<>("key0", "value0"), + new java.util.AbstractMap.SimpleEntry<>("key1", "value1"), + new java.util.AbstractMap.SimpleEntry<>("key2", (java.lang.String) null)) + .collect(java.util.HashMap::new, + (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll); + consumeTestCase(stringStringMapType, expectedMap, + "java.util.stream.Stream.<java.util.AbstractMap.SimpleEntry<java.lang.String, java.lang.String>>of(new java.util.AbstractMap.SimpleEntry<>(\"key0\", \"value0\"), new java.util.AbstractMap.SimpleEntry<>(\"key1\", \"value1\"), new java.util.AbstractMap.SimpleEntry<>(\"key2\", (java.lang.String) null)).collect(java.util.HashMap::new, (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll)", + Arrays.asList((byte) 1, // do not return null for the map + 32, // remaining bytes + (byte) 1, // do not return null for the string + 31, // remaining bytes + "key0", + (byte) 1, // do not return null for the string + 28, // remaining bytes + "value0", + 28, // remaining bytes + 28, // consumeArrayLength + (byte) 1, // do not return null for the string + 27, // remaining bytes + "key1", + (byte) 1, // do not return null for the string + 23, // remaining bytes + "value1", + (byte) 1, // do not return null for the string + 27, // remaining bytes + "key2", + (byte) 0 // *do* return null for the string + )); + } + + private Map<String, String> returnsStringStringMap() { + throw new IllegalStateException( + "Should not be called, only exists to construct its generic return type"); + } + + public static boolean isFive(int arg) { + return arg == 5; + } + + public static boolean intEquals(int arg1, int arg2) { + return arg1 == arg2; } @Test @@ -129,7 +169,7 @@ public class MetaTest { MetaTest.class.getMethod("isFive", int.class), Collections.singletonList(5)); autofuzzTestCase(false, "com.code_intelligence.jazzer.autofuzz.MetaTest.intEquals(5, 4)", MetaTest.class.getMethod("intEquals", int.class, int.class), Arrays.asList(5, 4)); - autofuzzTestCase("foobar", "\"foo\".concat(\"bar\")", + autofuzzTestCase("foobar", "(\"foo\").concat(\"bar\")", String.class.getMethod("concat", String.class), Arrays.asList((byte) 1, 6, "foo", (byte) 1, 6, "bar")); autofuzzTestCase("jazzer", "new java.lang.String(\"jazzer\")", diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java index 52f19a74..d556beb3 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java @@ -24,6 +24,7 @@ import java.io.ByteArrayInputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.List; class TestHelpers { @@ -57,8 +58,7 @@ class TestHelpers { } static void consumeTestCase( - Class<?> type, Object expectedResult, String expectedResultString, List<Object> cannedData) { - assertTrue(expectedResult == null || type.isAssignableFrom(expectedResult.getClass())); + Type type, Object expectedResult, String expectedResultString, List<Object> cannedData) { AutofuzzCodegenVisitor visitor = new AutofuzzCodegenVisitor(); FuzzedDataProvider data = CannedFuzzedDataProvider.create(cannedData); assertGeneralEquals(expectedResult, Meta.consume(data, type, visitor)); diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt index 53efd200..c5a2e156 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt @@ -14,11 +14,14 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File private fun applyAfterHooks(bytecode: ByteArray): ByteArray { - return HookInstrumentor(loadHooks(AfterHooks::class.java), false).instrument(bytecode) + val hooks = Hooks.loadHooks(setOf(AfterHooks::class.java.name)).first().hooks + return HookInstrumentor(hooks, false).instrument(bytecode) } private fun getOriginalAfterHooksTargetInstance(): AfterHooksTargetContract { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 472d2b98..036559ec 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -7,6 +7,7 @@ kt_jvm_library( "DynamicTestContract.java", "PatchTestUtils.kt", ], + visibility = ["//visibility:public"], ) wrapped_kt_jvm_test( @@ -130,6 +131,7 @@ wrapped_kt_jvm_test( size = "small", srcs = [ "ReplaceHooks.java", + "ReplaceHooksInit.java", "ReplaceHooksPatchTest.kt", "ReplaceHooksTarget.java", "ReplaceHooksTargetContract.java", diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt index 31e9733c..4fde7ee1 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt @@ -14,11 +14,14 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File private fun applyBeforeHooks(bytecode: ByteArray): ByteArray { - return HookInstrumentor(loadHooks(BeforeHooks::class.java), false).instrument(bytecode) + val hooks = Hooks.loadHooks(setOf(BeforeHooks::class.java.name)).first().hooks + return HookInstrumentor(hooks, false).instrument(bytecode) } private fun getOriginalBeforeHooksTargetInstance(): BeforeHooksTargetContract { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt index 15c88f4c..f2cf2f08 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt @@ -14,12 +14,37 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes import java.io.File import kotlin.test.assertEquals +/** + * Amends the instrumentation performed by [strategy] to call the map's public static void method + * updated() after every update to coverage counters. + */ +private fun makeTestable(strategy: EdgeCoverageStrategy): EdgeCoverageStrategy = + object : EdgeCoverageStrategy by strategy { + override fun instrumentControlFlowEdge( + mv: MethodVisitor, + edgeId: Int, + variable: Int, + coverageMapInternalClassName: String + ) { + strategy.instrumentControlFlowEdge(mv, edgeId, variable, coverageMapInternalClassName) + mv.visitMethodInsn(Opcodes.INVOKESTATIC, coverageMapInternalClassName, "updated", "()V", false) + } + } + private fun applyInstrumentation(bytecode: ByteArray): ByteArray { - return EdgeCoverageInstrumentor(0, MockCoverageMap::class.java).instrument(bytecode) + return EdgeCoverageInstrumentor( + makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), + MockCoverageMap::class.java, + 0 + ).instrument(bytecode) } private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract { @@ -41,26 +66,34 @@ private fun assertControlFlow(expectedLocations: List<Int>) { assertEquals(expectedLocations, MockCoverageMap.locations.toList()) } +@Suppress("unused") class CoverageInstrumentationTest { private val constructorReturn = 0 - private val ifFirstBranch = 1 - @Suppress("unused") - private val ifSecondBranch = 2 - private val ifEnd = 3 - private val outerForCondition = 4 - private val innerForBodyIfFirstRun = 6 - private val innerForBodyIfSecondRun = 5 - private val innerForIncrementCounter = 7 - private val outerForIncrementCounter = 8 - private val afterFooInvocation = 9 - private val beforeReturn = 10 - private val fooAfterBarInvocation = 11 - private val fooBeforeReturn = 12 - private val barAfterMapPutInvocation = 13 - private val barBeforeReturn = 14 - @Suppress("unused") - private val bazReturn = 15 + + private val mapConstructor = 1 + private val addFor0 = 2 + private val addFor1 = 3 + private val addFor2 = 4 + private val addFor3 = 5 + private val addFor4 = 6 + private val addFoobar = 7 + + private val ifTrueBranch = 8 + private val addBlock1 = 9 + private val ifFalseBranch = 10 + private val ifEnd = 11 + + private val outerForCondition = 12 + private val innerForCondition = 13 + private val innerForBodyIfTrueBranch = 14 + private val innerForBodyIfFalseBranch = 15 + private val innerForBodyPutInvocation = 16 + private val outerForIncrementCounter = 17 + + private val afterFooInvocation = 18 + private val fooAfterBarInvocation = 19 + private val barAfterPutInvocation = 20 @Test fun testOriginal() { @@ -72,31 +105,32 @@ class CoverageInstrumentationTest { MockCoverageMap.clear() assertSelfCheck(getInstrumentedInstrumentationTargetInstance()) - val innerForFirstRunControlFlow = mutableListOf<Int>().apply { + val mapControlFlow = listOf(mapConstructor, addFor0, addFor1, addFor2, addFor3, addFor4, addFoobar) + val ifControlFlow = listOf(ifTrueBranch, addBlock1, ifEnd) + val forFirstRunControlFlow = mutableListOf<Int>().apply { + add(outerForCondition) repeat(5) { - addAll(listOf(innerForBodyIfFirstRun, innerForIncrementCounter)) + addAll(listOf(innerForCondition, innerForBodyIfFalseBranch, innerForBodyPutInvocation)) } + add(outerForIncrementCounter) }.toList() - val innerForSecondRunControlFlow = mutableListOf<Int>().apply { + val forSecondRunControlFlow = mutableListOf<Int>().apply { + add(outerForCondition) repeat(5) { - addAll(listOf(innerForBodyIfSecondRun, innerForIncrementCounter)) + addAll(listOf(innerForCondition, innerForBodyIfTrueBranch, innerForBodyPutInvocation)) } + add(outerForIncrementCounter) }.toList() - val outerForControlFlow = - listOf(outerForCondition) + - innerForFirstRunControlFlow + - listOf(outerForIncrementCounter, outerForCondition) + - innerForSecondRunControlFlow + - listOf(outerForIncrementCounter) - + val forControlFlow = forFirstRunControlFlow + forSecondRunControlFlow + val fooCallControlFlow = listOf( + barAfterPutInvocation, fooAfterBarInvocation, afterFooInvocation + ) assertControlFlow( - listOf(constructorReturn, ifFirstBranch, ifEnd) + - outerForControlFlow + - listOf( - barAfterMapPutInvocation, barBeforeReturn, - fooAfterBarInvocation, fooBeforeReturn, - afterFooInvocation, beforeReturn - ) + listOf(constructorReturn) + + mapControlFlow + + ifControlFlow + + forControlFlow + + fooCallControlFlow ) } @@ -109,17 +143,17 @@ class CoverageInstrumentationTest { // The constructor of the target is run only once. val takenOnceEdge = constructorReturn // Control flows through the first if branch once per run. - val takenOnEveryRunEdge = ifFirstBranch + val takenOnEveryRunEdge = ifTrueBranch var lastCounter = 0.toUByte() for (i in 1..600) { assertSelfCheck(target) - assertEquals(1, MockCoverageMap.mem[takenOnceEdge]) + assertEquals(1, MockCoverageMap.counters[takenOnceEdge]) // Verify that the counter increments, but is never zero. val expectedCounter = (lastCounter + 1U).toUByte().takeUnless { it == 0.toUByte() } ?: (lastCounter + 2U).toUByte() lastCounter = expectedCounter - val actualCounter = MockCoverageMap.mem[takenOnEveryRunEdge].toUByte() + val actualCounter = MockCoverageMap.counters[takenOnEveryRunEdge].toUByte() assertEquals(expectedCounter, actualCounter, "After $i runs:") } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt index 7e7c31c9..ac263dc5 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt @@ -22,7 +22,8 @@ import kotlin.test.assertFailsWith class HookValidationTest { @Test fun testValidHooks() { - assertEquals(6, loadHooks(ValidHookMocks::class.java).size) + val hooks = Hooks.loadHooks(setOf(ValidHookMocks::class.java.name)).first().hooks + assertEquals(5, hooks.size) } @Test @@ -30,7 +31,8 @@ class HookValidationTest { for (method in InvalidHookMocks::class.java.methods) { if (method.isAnnotationPresent(MethodHook::class.java)) { assertFailsWith<IllegalArgumentException>("Expected ${method.name} to be an invalid hook") { - Hook.verifyAndGetHook(method, method.declaredAnnotations.first() as MethodHook) + val methodHook = method.declaredAnnotations.first() as MethodHook + Hook.createAndVerifyHook(method, methodHook, methodHook.targetClassName) } } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java index 2723ad6e..0df349ca 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java @@ -18,6 +18,7 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.MethodHook; import java.lang.invoke.MethodHandle; +@SuppressWarnings({"unused", "RedundantThrows"}) class InvalidHookMocks { @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.String", targetMethod = "equals") public static void incorrectHookIdType( @@ -45,7 +46,14 @@ class InvalidHookMocks { return true; } - @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.StringBuilder", + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.System", targetMethod = "gc", + targetMethodDescriptor = "()V") + public static Object + invalidReplaceVoidMethod(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return null; + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.StringBuilder", targetMethod = "<init>", targetMethodDescriptor = "(Ljava/lang/String;)V") public static Object invalidReturnType(MethodHandle method, Object thisObject, Object[] arguments, int hookId) @@ -58,4 +66,22 @@ class InvalidHookMocks { public static void primitiveReturnValueMustBeWrapped(MethodHandle method, String thisObject, Object[] arguments, int hookId, boolean returnValue) {} + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.StringBuilder", + targetMethod = "<init>", targetMethodDescriptor = "(Ljava/lang/String;)V") + public static void + replaceOnInitWithoutReturnType( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) throws Throwable {} + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.StringBuilder", + targetMethod = "<init>", targetMethodDescriptor = "(Ljava/lang/String;)V") + public static Object + replaceOnInitWithIncompatibleType( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) throws Throwable { + return new Object(); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + public static void primitiveReturnType(MethodHandle method, String thisObject, Object[] arguments, + int hookId, boolean returnValue) {} } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java index 787ea493..3ea33d19 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java @@ -20,8 +20,7 @@ import java.util.Arrays; public class MockCoverageMap { public static final int SIZE = 65536; - public static final ByteBuffer mem = ByteBuffer.allocate(SIZE); - public static int prev_location = 0; // is used in byte code directly + public static final ByteBuffer counters = ByteBuffer.allocate(SIZE); private static final ByteBuffer previous_mem = ByteBuffer.allocate(SIZE); public static ArrayList<Integer> locations = new ArrayList<>(); @@ -29,16 +28,25 @@ public class MockCoverageMap { public static void updated() { int updated_pos = -1; for (int i = 0; i < SIZE; i++) { - if (previous_mem.get(i) != mem.get(i)) { + if (previous_mem.get(i) != counters.get(i)) { updated_pos = i; } } locations.add(updated_pos); - System.arraycopy(mem.array(), 0, previous_mem.array(), 0, SIZE); + System.arraycopy(counters.array(), 0, previous_mem.array(), 0, SIZE); + } + + public static void enlargeIfNeeded(int nextId) { + // This mock coverage map is statically sized. + } + + public static void recordCoverage(int id) { + byte counter = counters.get(id); + counters.put(id, (byte) (counter == -1 ? 1 : counter + 1)); } public static void clear() { - Arrays.fill(mem.array(), (byte) 0); + Arrays.fill(counters.array(), (byte) 0); Arrays.fill(previous_mem.array(), (byte) 0); locations.clear(); } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt index f286d03f..00279c35 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt @@ -14,30 +14,40 @@ package com.code_intelligence.jazzer.instrumentor -fun classToBytecode(targetClass: Class<*>): ByteArray { - return ClassLoader - .getSystemClassLoader() - .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!! - .use { - it.readBytes() - } -} +import java.io.FileOutputStream -fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> { - return BytecodeClassLoader(name, bytecode).loadClass(name) -} +object PatchTestUtils { + @JvmStatic + fun classToBytecode(targetClass: Class<*>): ByteArray { + return ClassLoader + .getSystemClassLoader() + .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!! + .use { + it.readBytes() + } + } -/** - * A ClassLoader that dynamically loads a single specified class from byte code and delegates all other class loads to - * its own ClassLoader. - */ -class BytecodeClassLoader(val className: String, private val classBytecode: ByteArray) : - ClassLoader(BytecodeClassLoader::class.java.classLoader) { - override fun loadClass(name: String): Class<*> { - if (name != className) - return super.loadClass(name) + @JvmStatic + fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> { + return BytecodeClassLoader(name, bytecode).loadClass(name) + } + + @JvmStatic + public fun dumpBytecode(outDir: String, name: String, originalBytecode: ByteArray) { + FileOutputStream("$outDir/$name.class").use { fos -> fos.write(originalBytecode) } + } - return defineClass(className, classBytecode, 0, classBytecode.size) + /** + * A ClassLoader that dynamically loads a single specified class from byte code and delegates all other class loads to + * its own ClassLoader. + */ + class BytecodeClassLoader(val className: String, private val classBytecode: ByteArray) : + ClassLoader(BytecodeClassLoader::class.java.classLoader) { + override fun loadClass(name: String): Class<*> { + if (name != className) + return super.loadClass(name) + return defineClass(className, classBytecode, 0, classBytecode.size) + } } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java index a71e1180..7e31b77b 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java @@ -18,6 +18,7 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.MethodHook; import java.lang.invoke.MethodHandle; +@SuppressWarnings("unused") public class ReplaceHooks { @MethodHook(type = HookType.REPLACE, targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", @@ -106,4 +107,30 @@ public class ReplaceHooks { patchAbstractListGet(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { return true; } + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.util.Set", targetMethod = "contains", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + public static boolean + patchSetGet(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return true; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksInit", + targetMethod = "<init>", targetMethodDescriptor = "()V") + public static ReplaceHooksInit + patchInit(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + // Test with subclass + return new ReplaceHooksInit() { + { initialized = true; } + }; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksInit", + targetMethod = "<init>", targetMethodDescriptor = "(ZLjava/lang/String;)V") + public static ReplaceHooksInit + patchInitWithParams(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return new ReplaceHooksInit(true, ""); + } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java new file mode 100644 index 00000000..da77be81 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java @@ -0,0 +1,26 @@ +// Copyright 2021 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; + +public class ReplaceHooksInit { + public boolean initialized; + + public ReplaceHooksInit() {} + + @SuppressWarnings("unused") + public ReplaceHooksInit(boolean initialized, String ignored) { + this.initialized = initialized; + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt index 76fb53e5..b6266d12 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt @@ -14,11 +14,14 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File private fun applyReplaceHooks(bytecode: ByteArray): ByteArray { - return HookInstrumentor(loadHooks(ReplaceHooks::class.java), false).instrument(bytecode) + val hooks = Hooks.loadHooks(setOf(ReplaceHooks::class.java.name)).first().hooks + return HookInstrumentor(hooks, false).instrument(bytecode) } private fun getOriginalReplaceHooksTargetInstance(): ReplaceHooksTargetContract { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java index 7a4b89f8..fadbdf80 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java @@ -15,9 +15,9 @@ package com.code_intelligence.jazzer.instrumentor; import java.security.SecureRandom; -import java.util.AbstractList; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; // selfCheck() only passes with the hooks in ReplaceHooks.java applied. @@ -56,10 +56,16 @@ public class ReplaceHooksTarget implements ReplaceHooksTargetContract { shouldCallPass(); } - AbstractList<Boolean> boolList = new ArrayList<>(); + ArrayList<Boolean> boolList = new ArrayList<>(); boolList.add(false); results.put("arrayListGet", boolList.get(0)); + HashSet<Boolean> boolSet = new HashSet<>(); + results.put("stringSetGet", boolSet.contains(Boolean.TRUE)); + + results.put("shouldInitialize", new ReplaceHooksInit().initialized); + results.put("shouldInitializeWithParams", new ReplaceHooksInit(false, "foo").initialized); + return results; } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java index 48f16e60..d8e28881 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java @@ -37,6 +37,7 @@ public class TraceDataFlowInstrumentationTarget implements DynamicTestContract { volatile int switchValue = 1200; + @SuppressWarnings("ReturnValueIgnored") @Override public Map<String, Boolean> selfCheck() { Map<String, Boolean> results = new HashMap<>(); diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt index c6fd218f..4d4b0318 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt @@ -14,6 +14,8 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java index 06bed141..a919242b 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java @@ -27,10 +27,6 @@ class ValidHookMocks { public static void validAfterHook(MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean returnValue) {} - @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") - public static void validAfterHook2(MethodHandle method, String thisObject, Object[] arguments, - int hookId, boolean returnValue) {} - @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.String", targetMethod = "equals", targetMethodDescriptor = "(Ljava/lang/Object;)Z") public static Boolean diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..97ac4f62 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,38 @@ +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") + +java_test( + name = "FuzzedDataProviderImplTest", + srcs = ["FuzzedDataProviderImplTest.java"], + use_testrunner = False, + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + ], +) + +java_test( + name = "RecordingFuzzedDataProviderTest", + srcs = [ + "RecordingFuzzedDataProviderTest.java", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider", + "@maven//:junit_junit", + ], +) + +java_test( + name = "TraceCmpHooksTest", + srcs = [ + "TraceCmpHooksTest.java", + ], + target_compatible_with = SKIP_ON_WINDOWS, + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + "@maven//:junit_junit", + ], +) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImplTest.java b/agent/src/test/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImplTest.java new file mode 100644 index 00000000..5e922fc0 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImplTest.java @@ -0,0 +1,225 @@ +// Copyright 2021 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.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.Arrays; +import java.util.stream.Collectors; + +public class FuzzedDataProviderImplTest { + public static void main(String[] args) { + try (FuzzedDataProviderImpl fuzzedDataProvider = + FuzzedDataProviderImpl.withJavaData(INPUT_BYTES)) { + verifyFuzzedDataProvider(fuzzedDataProvider); + } + } + + private strictfp static void verifyFuzzedDataProvider(FuzzedDataProvider data) { + assertEqual(true, data.consumeBoolean()); + + assertEqual((byte) 0x7F, data.consumeByte()); + assertEqual((byte) 0x14, data.consumeByte((byte) 0x12, (byte) 0x22)); + + assertEqual(0x12345678, data.consumeInt()); + assertEqual(-0x12345600, data.consumeInt(-0x12345678, -0x12345600)); + assertEqual(0x12345679, data.consumeInt(0x12345678, 0x12345679)); + + assertEqual(true, Arrays.equals(new byte[] {0x01, 0x02}, data.consumeBytes(2))); + + assertEqual("jazzer", data.consumeString(6)); + assertEqual("ja\u0000zer", data.consumeString(6)); + assertEqual("€ß", data.consumeString(2)); + + assertEqual("jazzer", data.consumeAsciiString(6)); + assertEqual("ja\u0000zer", data.consumeAsciiString(6)); + assertEqual("\u0062\u0002\u002C\u0043\u001F", data.consumeAsciiString(5)); + + assertEqual(true, + Arrays.equals(new boolean[] {false, false, true, false, true}, data.consumeBooleans(5))); + assertEqual(true, + Arrays.equals(new long[] {0x0123456789abdcefL, 0xfedcba9876543210L}, data.consumeLongs(2))); + + assertEqual((float) 0.28969181, data.consumeProbabilityFloat()); + assertEqual(0.086814121166605432, data.consumeProbabilityDouble()); + assertEqual((float) 0.30104411, data.consumeProbabilityFloat()); + assertEqual(0.96218831486039413, data.consumeProbabilityDouble()); + + assertEqual((float) -2.8546307e+38, data.consumeRegularFloat()); + assertEqual(8.0940194040236032e+307, data.consumeRegularDouble()); + assertEqual((float) 271.49084, data.consumeRegularFloat((float) 123.0, (float) 777.0)); + assertEqual(30.859126145478349, data.consumeRegularDouble(13.37, 31.337)); + + assertEqual((float) 0.0, data.consumeFloat()); + assertEqual((float) -0.0, data.consumeFloat()); + assertEqual(Float.POSITIVE_INFINITY, data.consumeFloat()); + assertEqual(Float.NEGATIVE_INFINITY, data.consumeFloat()); + assertEqual(true, Float.isNaN(data.consumeFloat())); + assertEqual(Float.MIN_VALUE, data.consumeFloat()); + assertEqual(-Float.MIN_VALUE, data.consumeFloat()); + assertEqual(Float.MIN_NORMAL, data.consumeFloat()); + assertEqual(-Float.MIN_NORMAL, data.consumeFloat()); + assertEqual(Float.MAX_VALUE, data.consumeFloat()); + assertEqual(-Float.MAX_VALUE, data.consumeFloat()); + + assertEqual(0.0, data.consumeDouble()); + assertEqual(-0.0, data.consumeDouble()); + assertEqual(Double.POSITIVE_INFINITY, data.consumeDouble()); + assertEqual(Double.NEGATIVE_INFINITY, data.consumeDouble()); + assertEqual(true, Double.isNaN(data.consumeDouble())); + assertEqual(Double.MIN_VALUE, data.consumeDouble()); + assertEqual(-Double.MIN_VALUE, data.consumeDouble()); + assertEqual(Double.MIN_NORMAL, data.consumeDouble()); + assertEqual(-Double.MIN_NORMAL, data.consumeDouble()); + assertEqual(Double.MAX_VALUE, data.consumeDouble()); + assertEqual(-Double.MAX_VALUE, data.consumeDouble()); + + int[] array = {0, 1, 2, 3, 4}; + assertEqual(4, data.pickValue(array)); + assertEqual(2, (int) data.pickValue(Arrays.stream(array).boxed().toArray())); + assertEqual(3, data.pickValue(Arrays.stream(array).boxed().collect(Collectors.toList()))); + assertEqual(2, data.pickValue(Arrays.stream(array).boxed().collect(Collectors.toSet()))); + + // Buffer is almost depleted at this point. + assertEqual(7, data.remainingBytes()); + assertEqual(true, Arrays.equals(new long[0], data.consumeLongs(3))); + assertEqual(7, data.remainingBytes()); + assertEqual(true, Arrays.equals(new int[] {0x12345678}, data.consumeInts(3))); + assertEqual(3, data.remainingBytes()); + assertEqual(0x123456L, data.consumeLong()); + + // Buffer has been fully consumed at this point + assertEqual(0, data.remainingBytes()); + assertEqual(0, data.consumeInt()); + assertEqual(0.0, data.consumeDouble()); + assertEqual(-13.37, data.consumeRegularDouble(-13.37, 31.337)); + assertEqual(true, Arrays.equals(new byte[0], data.consumeBytes(4))); + assertEqual(true, Arrays.equals(new long[0], data.consumeLongs(4))); + assertEqual("", data.consumeRemainingAsAsciiString()); + assertEqual("", data.consumeRemainingAsString()); + assertEqual("", data.consumeAsciiString(100)); + assertEqual("", data.consumeString(100)); + } + + private static <T extends Comparable<T>> void assertEqual(T a, T b) { + if (a.compareTo(b) != 0) { + throw new IllegalArgumentException("Expected: " + a + ", got: " + b); + } + } + + private static final byte[] INPUT_BYTES = new byte[] { + // Bytes read from the start + 0x01, 0x02, // consumeBytes(2): {0x01, 0x02} + + 'j', 'a', 'z', 'z', 'e', 'r', // consumeString(6): "jazzer" + 'j', 'a', 0x00, 'z', 'e', 'r', // consumeString(6): "ja\u0000zer" + (byte) 0xE2, (byte) 0x82, (byte) 0xAC, (byte) 0xC3, (byte) 0x9F, // consumeString(2): "€ẞ" + + 'j', 'a', 'z', 'z', 'e', 'r', // consumeAsciiString(6): "jazzer" + 'j', 'a', 0x00, 'z', 'e', 'r', // consumeAsciiString(6): "ja\u0000zer" + (byte) 0xE2, (byte) 0x82, (byte) 0xAC, (byte) 0xC3, + (byte) 0x9F, // consumeAsciiString(5): "\u0062\u0002\u002C\u0043\u001F" + + 0, 0, 1, 0, 1, // consumeBooleans(5): { false, false, true, false, true } + (byte) 0xEF, (byte) 0xDC, (byte) 0xAB, (byte) 0x89, 0x67, 0x45, 0x23, 0x01, 0x10, 0x32, 0x54, + 0x76, (byte) 0x98, (byte) 0xBA, (byte) 0xDC, (byte) 0xFE, + // consumeLongs(2): { 0x0123456789ABCDEF, 0xFEDCBA9876543210 } + + 0x78, 0x56, 0x34, 0x12, // consumeInts(3): { 0x12345678 } + 0x56, 0x34, 0x12, // consumeLong(): + + // Bytes read from the end + 0x02, 0x03, 0x02, 0x04, // 4x pickValue in array with five elements + + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 10, // -max for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 9, // max for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 8, // -min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 7, // min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 6, // -denorm_min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 5, // denorm_min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 4, // NaN for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 3, // -infinity for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 2, // infinity for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 1, // -0.0 for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 0, // 0.0 for next consumeDouble + + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 10, // -max for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 9, // max for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 8, // -min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 7, // min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 6, // -denorm_min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 5, // denorm_min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 4, // NaN for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 3, // -infinity for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 2, // infinity for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 1, // -0.0 for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 0, // 0.0 for next consumeFloat + + (byte) 0x88, (byte) 0xAB, 0x61, (byte) 0xCB, 0x32, (byte) 0xEB, 0x30, (byte) 0xF9, + // consumeDouble(13.37, 31.337): 30.859126145478349 (small range) + 0x51, (byte) 0xF6, 0x1F, 0x3A, // consumeFloat(123.0, 777.0): 271.49084 (small range) + 0x11, 0x4D, (byte) 0xFD, 0x54, (byte) 0xD6, 0x3D, 0x43, 0x73, 0x39, + // consumeRegularDouble(): 8.0940194040236032e+307 + 0x16, (byte) 0xCF, 0x3D, 0x29, 0x4A, // consumeRegularFloat(): -2.8546307e+38 + + 0x61, (byte) 0xCB, 0x32, (byte) 0xEB, 0x30, (byte) 0xF9, 0x51, (byte) 0xF6, + // consumeProbabilityDouble(): 0.96218831486039413 + 0x1F, 0x3A, 0x11, 0x4D, // consumeProbabilityFloat(): 0.30104411 + (byte) 0xFD, 0x54, (byte) 0xD6, 0x3D, 0x43, 0x73, 0x39, 0x16, + // consumeProbabilityDouble(): 0.086814121166605432 + (byte) 0xCF, 0x3D, 0x29, 0x4A, // consumeProbabilityFloat(): 0.28969181 + + 0x01, // consumeInt(0x12345678, 0x12345679): 0x12345679 + 0x78, // consumeInt(-0x12345678, -0x12345600): -0x12345600 + 0x78, 0x56, 0x34, 0x12, // consumeInt(): 0x12345678 + + 0x02, // consumeByte(0x12, 0x22): 0x14 + 0x7F, // consumeByte(): 0x7F + + 0x01, // consumeBool(): true + }; +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProviderTest.java b/agent/src/test/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProviderTest.java new file mode 100644 index 00000000..d58a5ca9 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProviderTest.java @@ -0,0 +1,214 @@ +// Copyright 2021 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.code_intelligence.jazzer.api.CannedFuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import org.junit.Assert; +import org.junit.Test; + +public class RecordingFuzzedDataProviderTest { + @Test + public void testRecordingFuzzedDataProvider() throws IOException { + FuzzedDataProvider mockData = new MockFuzzedDataProvider(); + String referenceResult = sampleFuzzTarget(mockData); + + FuzzedDataProvider recordingMockData = + RecordingFuzzedDataProvider.makeFuzzedDataProviderProxy(mockData); + Assert.assertEquals(referenceResult, sampleFuzzTarget(recordingMockData)); + + String cannedMockDataString = + RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy(recordingMockData); + FuzzedDataProvider cannedMockData = new CannedFuzzedDataProvider(cannedMockDataString); + Assert.assertEquals(referenceResult, sampleFuzzTarget(cannedMockData)); + } + + private String sampleFuzzTarget(FuzzedDataProvider data) { + StringBuilder result = new StringBuilder(); + result.append(data.consumeString(10)); + int[] ints = data.consumeInts(5); + result.append(Arrays.stream(ints).mapToObj(Integer::toString).collect(Collectors.joining(","))); + result.append(data.pickValue(ints)); + result.append(data.consumeString(20)); + result.append(data.pickValues(Arrays.stream(ints).boxed().collect(Collectors.toSet()), 5) + .stream() + .map(Integer::toHexString) + .collect(Collectors.joining(","))); + result.append(data.remainingBytes()); + return result.toString(); + } + + private static final class MockFuzzedDataProvider implements FuzzedDataProvider { + @Override + public boolean consumeBoolean() { + return true; + } + + @Override + public boolean[] consumeBooleans(int maxLength) { + return new boolean[] {false, true}; + } + + @Override + public byte consumeByte() { + return 2; + } + + @Override + public byte consumeByte(byte min, byte max) { + return max; + } + + @Override + public short consumeShort() { + return 2; + } + + @Override + public short consumeShort(short min, short max) { + return min; + } + + @Override + public short[] consumeShorts(int maxLength) { + return new short[] {2, 4, 7}; + } + + @Override + public int consumeInt() { + return 5; + } + + @Override + public int consumeInt(int min, int max) { + return max; + } + + @Override + public int[] consumeInts(int maxLength) { + return IntStream.range(0, maxLength).toArray(); + } + + @Override + public long consumeLong() { + return 42; + } + + @Override + public long consumeLong(long min, long max) { + return min; + } + + @Override + public long[] consumeLongs(int maxLength) { + return LongStream.range(0, maxLength).toArray(); + } + + @Override + public float consumeFloat() { + return Float.NaN; + } + + @Override + public float consumeRegularFloat() { + return 0.3f; + } + + @Override + public float consumeRegularFloat(float min, float max) { + return min; + } + + @Override + public float consumeProbabilityFloat() { + return 0.2f; + } + + @Override + public double consumeDouble() { + return Double.NaN; + } + + @Override + public double consumeRegularDouble(double min, double max) { + return max; + } + + @Override + public double consumeRegularDouble() { + return Math.PI; + } + + @Override + public double consumeProbabilityDouble() { + return 0.5; + } + + @Override + public char consumeChar() { + return 'C'; + } + + @Override + public char consumeChar(char min, char max) { + return min; + } + + @Override + public char consumeCharNoSurrogates() { + return 'C'; + } + + @Override + public String consumeAsciiString(int maxLength) { + return "foobar"; + } + + @Override + public String consumeString(int maxLength) { + return "foo€ä"; + } + + @Override + public String consumeRemainingAsAsciiString() { + return "foobar"; + } + + @Override + public String consumeRemainingAsString() { + return "foobar"; + } + + @Override + public byte[] consumeBytes(int maxLength) { + return new byte[maxLength]; + } + + @Override + public byte[] consumeRemainingAsBytes() { + return new byte[] {1}; + } + + @Override + public int remainingBytes() { + return 1; + } + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/TraceCmpHooksTest.java b/agent/src/test/java/com/code_intelligence/jazzer/runtime/TraceCmpHooksTest.java new file mode 100644 index 00000000..9275ca30 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/TraceCmpHooksTest.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.runtime; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TraceCmpHooksTest { + private static final ExecutorService ES = Executors.newFixedThreadPool(5); + + @Test + public void cmpHookShouldHandleConcurrentModifications() throws InterruptedException { + String arg = "test"; + Map<String, Object> map = new HashMap<>(); + map.put(arg, arg); + + // Add elements to map asynchronously + Function<Integer, Runnable> put = (final Integer num) -> () -> { + map.put(String.valueOf(num), num); + }; + for (int i = 0; i < 1_000_000; i++) { + ES.submit(put.apply(i)); + } + + // Call hook + for (int i = 0; i < 1_000; i++) { + TraceCmpHooks.mapGet(null, map, new Object[] {arg}, 1, null); + } + + ES.shutdown(); + // noinspection ResultOfMethodCallIgnored + ES.awaitTermination(5, TimeUnit.SECONDS); + } +} |