diff options
Diffstat (limited to 'src/main/java/com/code_intelligence/jazzer/runtime')
14 files changed, 1452 insertions, 0 deletions
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..c31c86e4 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,180 @@ +load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar") +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") +load("//bazel:jar.bzl", "strip_jar") + +# The transitive dependencies of this target will be appended to the search path +# of the bootstrap class loader. They will be visible to all classes - care must +# be taken to shade everything and generally keep this target as small as +# possible. +java_binary( + name = "jazzer_bootstrap_unshaded", + create_executable = False, + runtime_deps = [":jazzer_bootstrap_lib"], +) + +java_library( + name = "jazzer_bootstrap_lib", + visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"], + runtime_deps = [ + ":runtime", + "//sanitizers", + ], +) + +# These classes with public Bazel visibility are contained in jazzer_bootstrap.jar +# and will thus be available on the bootstrap class path. This target can be +# passed to the `deploy_env` attribute of the Jazzer `java_binary` to ensure that +# it doesn't bundle in these classes. +java_binary( + name = "jazzer_bootstrap_env", + create_executable = False, + visibility = ["//src/main/java/com/code_intelligence/jazzer:__pkg__"], + runtime_deps = [ + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + +jar_jar( + name = "jazzer_bootstrap_unstripped", + input_jar = ":jazzer_bootstrap_unshaded_deploy.jar", + rules = "bootstrap_shade_rules", +) + +strip_jar( + name = "jazzer_bootstrap", + out = "jazzer_bootstrap.jar", + jar = ":jazzer_bootstrap_unstripped", + paths_to_keep = [ + "com/code_intelligence/jazzer/**", + "jaz/**", + "META-INF/MANIFEST.MF", + ], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/agent:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/android:__pkg__", + ], +) + +sh_test( + name = "jazzer_bootstrap_shading_test", + srcs = ["verify_shading.sh"], + args = [ + "$(rootpath jazzer_bootstrap.jar)", + ], + data = [ + "jazzer_bootstrap.jar", + "@local_jdk//:bin/jar", + ], + tags = [ + # Coverage instrumentation necessarily adds files to the jar that we + # wouldn't want to release and thus causes this test to fail. + "no-coverage", + ], + target_compatible_with = SKIP_ON_WINDOWS, +) + +# At runtime, the AgentInstaller appends jazzer_bootstrap.jar to the bootstrap +# class loader's search path - these classes must not be available on the +# regular classpath. Since dependents should not have to resort to reflection to +# access these classes they know will be there at runtime, this compile-time +# only dependency can be used as a replacement. +java_library( + name = "jazzer_bootstrap_compile_only", + neverlink = True, + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/autofuzz:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//src/main/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + ], + exports = [ + ":fuzz_target_runner_natives", + ":runtime", + ], +) + +# The following targets must only be referenced directly by tests or native implementations. + +java_jni_library( + name = "coverage_map", + srcs = ["CoverageMap.java"], + native_libs = select({ + "@platforms//os:android": ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"], + "//conditions:default": [], + }), + visibility = [ + "//src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//src/test:__subpackages__", + ], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + +java_jni_library( + name = "trace_data_flow_native_callbacks", + srcs = ["TraceDataFlowNativeCallbacks.java"], + visibility = [ + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], + deps = ["@org_ow2_asm_asm//jar"], +) + +java_jni_library( + name = "fuzz_target_runner_natives", + srcs = ["FuzzTargetRunnerNatives.java"], + visibility = ["//src/main/native/com/code_intelligence/jazzer/driver:__pkg__"], + deps = [ + ":constants", + ], +) + +java_jni_library( + name = "mutator", + srcs = ["Mutator.java"], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/mutator/libfuzzer:__pkg__", + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], +) + +java_library( + name = "runtime", + srcs = [ + "HardToCatchError.java", + "JazzerInternal.java", + "NativeLibHooks.java", + "TraceCmpHooks.java", + "TraceDivHooks.java", + "TraceIndirHooks.java", + ], + visibility = [ + "//src/main/java/com/code_intelligence/jazzer/android:__pkg__", + "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//src/test:__subpackages__", + ], + runtime_deps = [ + ":fuzz_target_runner_natives", + ":mutator", + # Access to Unsafe is possible without any tricks if the class that does it is loaded by the + # bootstrap loader. We thus want Jazzer to use this class from jazzer_bootstrap. + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], + deps = [ + ":constants", + ":coverage_map", + ":trace_data_flow_native_callbacks", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + ], +) + +# This target exposes a class that can safely be loaded in both the system and the bootstrap class +# loader as it provides true constants that do not change over the lifetime of the JVM. +java_library( + name = "constants", + srcs = ["Constants.java"], + visibility = ["//visibility:public"], +) diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java b/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java new file mode 100644 index 00000000..92f4a3ca --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/Constants.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 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; + +public final class Constants { + public static final boolean IS_ANDROID = System.getProperty("java.vm.vendor").contains("Android"); +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java new file mode 100644 index 00000000..a945a30a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java @@ -0,0 +1,169 @@ +// 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 static com.code_intelligence.jazzer.runtime.Constants.IS_ANDROID; + +import com.code_intelligence.jazzer.utils.UnsafeProvider; +import com.github.fmeum.rules_jni.RulesJni; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +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 counters are shared directly with native code. + */ +final public class CoverageMap { + 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; + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final Class<?> LOG; + private static final MethodHandle LOG_INFO; + private static final MethodHandle LOG_ERROR; + + static { + try { + LOG = Class.forName( + "com.code_intelligence.jazzer.utils.Log", false, ClassLoader.getSystemClassLoader()); + LOG_INFO = MethodHandles.lookup().findStatic( + LOG, "info", MethodType.methodType(void.class, String.class)); + LOG_ERROR = MethodHandles.lookup().findStatic( + LOG, "error", MethodType.methodType(void.class, String.class, Throwable.class)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * 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) { + logError( + String.format( + "Maximum number (%s) of coverage counters exceeded. Try to limit the scope of a single fuzz target as " + + "much as possible to keep the fuzzer fast. If that is not possible, the maximum number of " + + "counters can be increased via the %s environment variable.", + MAX_NUM_COUNTERS, ENV_MAX_NUM_COUNTERS), + null); + System.exit(1); + } + } + if (newNumCounters > currentNumCounters) { + registerNewCounters(currentNumCounters, newNumCounters); + currentNumCounters = newNumCounters; + logInfo("New number of coverage counters: " + currentNumCounters); + } + } + + // Called by the coverage instrumentation. + @SuppressWarnings("unused") + public static void recordCoverage(final int id) { + if (IS_ANDROID) { + enlargeIfNeeded(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); + } + } + + private static void logInfo(String message) { + try { + LOG_INFO.invokeExact(message); + } catch (Throwable error) { + // Should not be reached, Log.error does not throw. + error.printStackTrace(); + System.err.println("Failed to call Log.info:"); + System.err.println(message); + } + } + + private static void logError(String message, Throwable t) { + try { + LOG_ERROR.invokeExact(message, t); + } catch (Throwable error) { + // Should not be reached, Log.error does not throw. + error.printStackTrace(); + System.err.println("Failed to call Log.error:"); + System.err.println(message); + } + } + + // 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/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java b/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java new file mode 100644 index 00000000..bbf74fdb --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/FuzzTargetRunnerNatives.java @@ -0,0 +1,41 @@ +// 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; + +/** + * The native functions used by FuzzTargetRunner. + * + * <p>This class has to be loaded by the bootstrap class loader since the native library it loads + * links in libFuzzer and the Java hooks, which have to be on the bootstrap path so that they are + * seen by Java standard library classes, need to be able to call native libFuzzer callbacks. + */ +public class FuzzTargetRunnerNatives { + static { + if (!Constants.IS_ANDROID && FuzzTargetRunnerNatives.class.getClassLoader() != null) { + throw new IllegalStateException( + "FuzzTargetRunnerNatives must be loaded in the bootstrap loader"); + } + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + + public static native int startLibFuzzer( + byte[][] args, Class<?> runner, boolean useExperimentalMutator); + + public static native void printCrashingInput(); + + public static native void temporarilyDisableLibfuzzerExitHook(); +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java b/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java new file mode 100644 index 00000000..cf136051 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/HardToCatchError.java @@ -0,0 +1,82 @@ +// 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 java.io.PrintStream; +import java.io.PrintWriter; + +/** + * An Error that rethrows itself when any of its getters is invoked. + */ +public class HardToCatchError extends Error { + public HardToCatchError() { + super(); + } + + @Override + public String getMessage() { + throw this; + } + + @Override + public String getLocalizedMessage() { + throw this; + } + + @Override + public synchronized Throwable initCause(Throwable cause) { + throw this; + } + + @Override + public String toString() { + throw this; + } + + @Override + public void printStackTrace() { + throw this; + } + + @Override + public void printStackTrace(PrintStream s) { + throw this; + } + + @Override + public void printStackTrace(PrintWriter s) { + throw this; + } + + @Override + public StackTraceElement[] getStackTrace() { + throw this; + } + + @Override + public int hashCode() { + throw this; + } + + @Override + public boolean equals(Object obj) { + throw this; + } + + @Override + public Object clone() { + throw this; + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java new file mode 100644 index 00000000..3b368531 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java @@ -0,0 +1,50 @@ +// 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 java.util.ArrayList; + +final public class JazzerInternal { + public static Throwable lastFinding; + // The value is only relevant when regression testing. Read by the bytecode emitted by + // HookMethodVisitor to enable hooks only when invoked from a @FuzzTest. + // + // Alternatives considered: + // Making this thread local rather than global may potentially allow to run fuzz tests in + // parallel with regular unit tests, but it is next to impossible to determine which thread is + // currently doing work for a fuzz test versus a regular unit test. Instead, @FuzzTest is + // annotated with @Isolated. + @SuppressWarnings("unused") public static boolean hooksEnabled = true; + + private static final ArrayList<Runnable> onFuzzTargetReadyCallbacks = new ArrayList<>(); + + // Accessed from api.Jazzer via reflection. + public static void reportFindingFromHook(Throwable finding) { + lastFinding = finding; + // Throw an Error that is hard to catch (short of outright ignoring it) in order to quickly + // terminate the execution of the fuzz target. The finding will be reported as soon as the fuzz + // target returns even if this Error is swallowed. + throw new HardToCatchError(); + } + + public static void registerOnFuzzTargetReadyCallback(Runnable callback) { + onFuzzTargetReadyCallbacks.add(callback); + } + + public static void onFuzzTargetReady(String fuzzTargetClass) { + onFuzzTargetReadyCallbacks.forEach(Runnable::run); + onFuzzTargetReadyCallbacks.clear(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java b/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java new file mode 100644 index 00000000..2d9a7f65 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/Mutator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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 Mutator { + public static final boolean SHOULD_MOCK = + Boolean.parseBoolean(System.getenv("JAZZER_MOCK_LIBFUZZER_MUTATOR")); + + static { + if (!SHOULD_MOCK) { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + } + + public static native int defaultMutateNative(byte[] buffer, int size); + + private Mutator() {} +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java new file mode 100644 index 00000000..8572f05a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/NativeLibHooks.java @@ -0,0 +1,39 @@ +// 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.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +@SuppressWarnings("unused") +final public class NativeLibHooks { + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", + targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", + targetMethod = "loadLibrary", targetMethodDescriptor = "(Ljava/lang/String;)V") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "load", + targetMethodDescriptor = "(Ljava/lang/String;)V") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "load", + targetMethodDescriptor = "(Ljava/lang/String;)V") + public static void + loadLibraryHook(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (Constants.IS_ANDROID) { + return; + } + + TraceDataFlowNativeCallbacks.handleLibraryLoad(); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java new file mode 100644 index 00000000..7fb15866 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java @@ -0,0 +1,598 @@ +// 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.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.util.*; + +@SuppressWarnings("unused") +final public class TraceCmpHooks { + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", targetMethod = "compare", + targetMethodDescriptor = "(BB)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(BB)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", targetMethod = "compare", + targetMethodDescriptor = "(SS)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(SS)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "compare", targetMethodDescriptor = "(II)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(II)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "kotlin.jvm.internal.Intrinsics ", + targetMethod = "compare", targetMethodDescriptor = "(II)I") + public static void + integerCompare(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpInt((int) arguments[0], (int) arguments[1], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Byte", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Byte;)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Short", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Short;)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Integer;)I") + public static void + integerCompareTo(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpInt((int) thisObject, (int) arguments[0], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", targetMethod = "compare", + targetMethodDescriptor = "(JJ)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "compareUnsigned", targetMethodDescriptor = "(JJ)I") + public static void + longCompare(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpLong((long) arguments[0], (long) arguments[1], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "compareTo", targetMethodDescriptor = "(Ljava/lang/Long;)I") + public static void + longCompareTo(MethodHandle method, Long thisObject, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpLong(thisObject, (long) arguments[0], hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "kotlin.jvm.internal.Intrinsics ", + targetMethod = "compare", targetMethodDescriptor = "(JJ)I") + public static void + longCompareKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) { + TraceDataFlowNativeCallbacks.traceCmpLong((long) arguments[0], (long) arguments[1], hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "equalsIgnoreCase") + public static void + equals(MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean areEqual) { + if (!areEqual && arguments.length == 1 && arguments[0] instanceof String) { + // The precise value of the result of the comparison is not used by libFuzzer as long as it is + // non-zero. + TraceDataFlowNativeCallbacks.traceStrcmp(thisObject, (String) arguments[0], 1, hookId); + } + } + + @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 areEqual) { + if (!areEqual && arguments.length == 1 && arguments[0] != null + && thisObject.getClass() == arguments[0].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(thisObject, arguments[0], hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "clojure.lang.Util", targetMethod = "equiv") + public static void genericStaticEquals( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean areEqual) { + if (!areEqual && arguments.length == 2 && arguments[0] != null && arguments[1] != null + && arguments[1].getClass() == arguments[0].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(arguments[0], arguments[1], hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "compareTo") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "compareToIgnoreCase") + public static void + compareTo( + MethodHandle method, String thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue != 0 && arguments.length == 1 && arguments[0] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrcmp( + thisObject, (String) arguments[0], returnValue, hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contentEquals") + public static void + contentEquals(MethodHandle method, String thisObject, Object[] arguments, int hookId, + Boolean areEqualContents) { + if (!areEqualContents && arguments.length == 1 && arguments[0] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrcmp( + thisObject, ((CharSequence) arguments[0]).toString(), 1, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "regionMatches", targetMethodDescriptor = "(ZILjava/lang/String;II)Z") + public static void + regionsMatches5( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (!returnValue) { + int toffset = (int) arguments[1]; + String other = (String) arguments[2]; + int ooffset = (int) arguments[3]; + int len = (int) arguments[4]; + regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "regionMatches", targetMethodDescriptor = "(ILjava/lang/String;II)Z") + public static void + regionMatches4( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (!returnValue) { + int toffset = (int) arguments[0]; + String other = (String) arguments[1]; + int ooffset = (int) arguments[2]; + int len = (int) arguments[3]; + regionMatchesInternal((String) thisObject, toffset, other, ooffset, len, hookId); + } + } + + private static void regionMatchesInternal( + String thisString, int toffset, String other, int ooffset, int len, int hookId) { + if (toffset < 0 || ooffset < 0) + return; + int cappedThisStringEnd = Math.min(toffset + len, thisString.length()); + int cappedOtherStringEnd = Math.min(ooffset + len, other.length()); + String thisPart = thisString.substring(toffset, cappedThisStringEnd); + String otherPart = other.substring(ooffset, cappedOtherStringEnd); + TraceDataFlowNativeCallbacks.traceStrcmp(thisPart, otherPart, 1, hookId); + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "contains") + public static void + contains( + MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean doesContain) { + if (!doesContain && arguments.length == 1 && arguments[0] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + thisObject, ((CharSequence) arguments[0]).toString(), hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "indexOf") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "lastIndexOf") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.StringBuffer", targetMethod = "indexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuffer", + targetMethod = "lastIndexOf") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", targetMethod = "indexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", + targetMethod = "lastIndexOf") + public static void + indexOf( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == -1 && arguments.length >= 1 && arguments[0] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrstr( + thisObject.toString(), (String) arguments[0], hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "startsWith") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "endsWith") + public static void + startsWith(MethodHandle method, String thisObject, Object[] arguments, int hookId, + Boolean doesStartOrEndsWith) { + if (!doesStartOrEndsWith && arguments.length >= 1 && arguments[0] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrstr(thisObject, (String) arguments[0], hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "replace", + targetMethodDescriptor = + "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;") + public static void + replace( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) { + String original = (String) thisObject; + // Report only if the replacement was not successful. + if (original.equals(returnValue)) { + String target = arguments[0].toString(); + TraceDataFlowNativeCallbacks.traceStrstr(original, target, hookId); + } + } + + // For standard Kotlin packages, which are named according to the pattern kotlin.*, we append a + // whitespace to the package name of the target class so that they are not mangled due to shading. + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.jvm.internal.Intrinsics ", + targetMethod = "areEqual") + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "equals") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "equals$default") + public static void + equalsKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean equalStrings) { + if (!equalStrings && arguments.length >= 2 && arguments[0] instanceof String + && arguments[1] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrcmp( + (String) arguments[0], (String) arguments[1], 1, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "contentEquals") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "contentEquals$default") + public static void + contentEqualKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean equalStrings) { + if (!equalStrings && arguments.length >= 2 && arguments[0] instanceof CharSequence + && arguments[1] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrcmp( + arguments[0].toString(), arguments[1].toString(), 1, hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "compareTo") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "compareTo$default") + public static void + compareToKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue != 0 && arguments.length >= 2 && arguments[0] instanceof String + && arguments[1] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrcmp( + (String) arguments[0], (String) arguments[1], 1, hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "endsWith") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "endsWith$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "startsWith") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "startsWith$default") + public static void + startsWithKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean doesStartOrEndsWith) { + if (!doesStartOrEndsWith && arguments.length >= 2 && arguments[0] instanceof CharSequence + && arguments[1] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), arguments[1].toString(), hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "contains") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "contains$default") + public static void + containsKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Boolean doesContain) { + if (!doesContain && arguments.length >= 2 && arguments[0] instanceof CharSequence + && arguments[1] instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), arguments[1].toString(), hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "indexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "indexOf$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOf$default") + public static void + indexOfKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue != -1 || arguments.length < 2 || !(arguments[0] instanceof CharSequence)) { + return; + } + if (arguments[1] instanceof String) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), (String) arguments[1], hookId); + } else if (arguments[1] instanceof Character) { + TraceDataFlowNativeCallbacks.traceStrstr( + arguments[0].toString(), ((Character) arguments[1]).toString(), hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "replace") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replace$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfter") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfter$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfterLast") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceAfterLast$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBefore") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBefore$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBeforeLast") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceBeforeLast$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceFirst") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "replaceFirst$default") + public static void + replaceKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, String returnValue) { + if (arguments.length < 2 || !(arguments[0] instanceof String)) { + return; + } + String original = (String) arguments[0]; + if (!original.equals(returnValue)) { + return; + } + + // We currently don't handle the overloads that take a regex as a second argument. + if (arguments[1] instanceof String || arguments[1] instanceof Character) { + TraceDataFlowNativeCallbacks.traceStrstr(original, arguments[1].toString(), hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "regionMatches", + targetMethodDescriptor = "(Ljava/lang/String;ILjava/lang/String;IIZ)Z") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "regionMatches$default", + targetMethodDescriptor = "(Ljava/lang/String;ILjava/lang/String;IIZILjava/lang/Object;)Z") + public static void + regionMatchesKt(MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, + Boolean doesRegionMatch) { + if (!doesRegionMatch) { + String thisString = arguments[0].toString(); + int thisOffset = (int) arguments[1]; + String other = arguments[2].toString(); + int otherOffset = (int) arguments[3]; + int length = (int) arguments[4]; + regionMatchesInternal(thisString, thisOffset, other, otherOffset, length, hookId); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "indexOfAny") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "indexOfAny$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOfAny") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "lastIndexOfAny$default") + public static void + indexOfAnyKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == -1 && arguments.length >= 2 && arguments[0] instanceof CharSequence) { + guideTowardContainmentOfFirstElement(arguments[0].toString(), arguments[1], hookId); + } + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", targetMethod = "findAnyOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "findAnyOf$default") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "findLastAnyOf") + @MethodHook(type = HookType.AFTER, targetClassName = "kotlin.text.StringsKt ", + targetMethod = "findLastAnyOf$default") + public static void + findAnyKt( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId, Object returnValue) { + if (returnValue == null && arguments.length >= 2 && arguments[0] instanceof CharSequence) { + guideTowardContainmentOfFirstElement(arguments[0].toString(), arguments[1], hookId); + } + } + + private static void guideTowardContainmentOfFirstElement( + String containingString, Object candidateCollectionObj, int hookId) { + if (candidateCollectionObj instanceof Collection<?>) { + Collection<?> strings = (Collection<?>) candidateCollectionObj; + if (strings.isEmpty()) { + return; + } + Object firstElementObj = strings.iterator().next(); + if (firstElementObj instanceof CharSequence) { + TraceDataFlowNativeCallbacks.traceStrstr( + containingString, firstElementObj.toString(), hookId); + } + } else if (candidateCollectionObj.getClass().isArray()) { + if (candidateCollectionObj.getClass().getComponentType() == char.class) { + char[] chars = (char[]) candidateCollectionObj; + if (chars.length > 0) { + TraceDataFlowNativeCallbacks.traceStrstr( + containingString, Character.toString(chars[0]), hookId); + } + } + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals", + targetMethodDescriptor = "([B[B)Z") + 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]; + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals", + targetMethodDescriptor = "([BII[BII)Z") + 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]); + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", + targetMethodDescriptor = "([B[B)I") + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", + targetMethod = "compareUnsigned", targetMethodDescriptor = "([B[B)I") + 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]; + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", + targetMethodDescriptor = "([BII[BII)I") + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", + targetMethod = "compareUnsigned", targetMethodDescriptor = "([BII[BII)I") + 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]); + 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; + + @SuppressWarnings({"rawtypes", "unchecked"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Map", targetMethod = "get") + public static void mapGet( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + if (returnValue != null) + return; + if (arguments.length != 1) { + return; + } + if (thisObject == null) + return; + final Map map = (Map) thisObject; + if (map.size() == 0) + return; + final Object currentKey = arguments[0]; + if (currentKey == null) + return; + // Find two valid map keys that bracket currentKey. + // This is a generalization of libFuzzer's __sanitizer_cov_trace_switch: + // https://github.com/llvm/llvm-project/blob/318942de229beb3b2587df09e776a50327b5cef0/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L564 + Object lowerBoundKey = null; + Object upperBoundKey = null; + 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. + } + } 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; + } + } + } 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) { + TraceDataFlowNativeCallbacks.traceGenericCmp( + currentKey, lowerBoundKey, hookId + lowerBoundKey.hashCode()); + } + if (upperBoundKey != null) { + TraceDataFlowNativeCallbacks.traceGenericCmp( + currentKey, upperBoundKey, hookId + upperBoundKey.hashCode()); + } + } + + @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions", + targetMethod = "assertNotEquals", + targetMethodDescriptor = "(Ljava/lang/Object;Ljava/lang/Object;)V") + @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions", + targetMethod = "assertNotEquals", + targetMethodDescriptor = "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;)V") + @MethodHook(type = HookType.AFTER, targetClassName = "org.junit.jupiter.api.Assertions", + targetMethod = "assertNotEquals", + targetMethodDescriptor = + "(Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/Supplier;)V") + public static void + assertEquals(MethodHandle method, Object node, Object[] args, int hookId, Object alwaysNull) { + if (args[0] != null && args[1] != null && args[0].getClass() == args[1].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(args[0], args[1], hookId); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java new file mode 100644 index 00000000..777eb0a2 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java @@ -0,0 +1,125 @@ +// 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.github.fmeum.rules_jni.RulesJni; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.Arrays; +import org.objectweb.asm.Type; + +@SuppressWarnings("unused") +final public class TraceDataFlowNativeCallbacks { + // 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"); + + static { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + + // It is possible for RulesJni#loadLibrary to trigger a hook even though it isn't instrumented if + // it uses regexes, which it does with at least some JDKs due to its use of String#format. This + // led to exceptions in the past when the hook ended up calling traceStrcmp or traceStrstr before + // the static initializer was run: FUZZED_DATA_CHARSET used to be initialized after the call and + // thus still had the value null when encodeForLibFuzzer was called, resulting in an NPE in + // String#getBytes(Charset). Just switching the order may actually make this bug worse: It could + // now lead to traceMemcmp being called before the native library has been loaded. We guard + // against this by making the hooks noops when static initialization of this class hasn't + // completed yet. + private static final boolean NATIVE_INITIALIZED = true; + + public static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + + public static void traceStrcmp(String s1, String s2, int result, int pc) { + if (NATIVE_INITIALIZED) { + traceMemcmp(encodeForLibFuzzer(s1), encodeForLibFuzzer(s2), result, pc); + } + } + + public static void traceStrstr(String s1, String s2, int pc) { + if (NATIVE_INITIALIZED) { + traceStrstr0(encodeForLibFuzzer(s2), pc); + } + } + + public static void traceReflectiveCall(Executable callee, int pc) { + String className = callee.getDeclaringClass().getCanonicalName(); + String executableName = callee.getName(); + String descriptor; + if (callee instanceof Method) { + descriptor = Type.getMethodDescriptor((Method) callee); + } else { + descriptor = Type.getConstructorDescriptor((Constructor<?>) callee); + } + tracePcIndir(Arrays.hashCode(new String[] {className, executableName, descriptor}), pc); + } + + public static int traceCmpLongWrapper(long arg1, long arg2, int pc) { + traceCmpLong(arg1, arg2, pc); + // Long.compare serves as a substitute for the lcmp opcode, which can't be used directly + // as the stack layout required for the call can't be achieved without local variables. + return Long.compare(arg1, arg2); + } + + // 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 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/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java new file mode 100644 index 00000000..c4991eb5 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceDivHooks.java @@ -0,0 +1,47 @@ +// 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.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +@SuppressWarnings("unused") +final public class TraceDivHooks { + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "divideUnsigned", targetMethodDescriptor = "(II)I") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Integer", + targetMethod = "remainderUnsigned", targetMethodDescriptor = "(II)I") + public static void + intUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + // Since the arguments are to be treated as unsigned integers we need a long to fit the + // divisor. + TraceDataFlowNativeCallbacks.traceDivLong(Integer.toUnsignedLong((int) arguments[1]), hookId); + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "divideUnsigned", targetMethodDescriptor = "(JJ)J") + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Long", + targetMethod = "remainderUnsigned", targetMethodDescriptor = "(JJ)J") + public static void + longUnsignedDivide(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + long divisor = (long) arguments[1]; + // Run the callback only if the divisor, which is regarded as an unsigned long, fits in a + // signed long, i.e., does not have the sign bit set. + if (divisor > 0) { + TraceDataFlowNativeCallbacks.traceDivLong(divisor, hookId); + } + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java new file mode 100644 index 00000000..897ede6c --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceIndirHooks.java @@ -0,0 +1,35 @@ +// 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.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Executable; + +@SuppressWarnings("unused") +final public class TraceIndirHooks { + // The reflection hook is of type AFTER as it should only report calls that did not fail because + // of incorrect arguments passed. + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.reflect.Method", targetMethod = "invoke") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.reflect.Constructor", + targetMethod = "newInstance") + public static void + methodInvoke( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + TraceDataFlowNativeCallbacks.traceReflectiveCall((Executable) thisObject, hookId); + } +} diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules b/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules new file mode 100644 index 00000000..0cafcf0a --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/bootstrap_shade_rules @@ -0,0 +1,4 @@ +rule com.github.fmeum.rules_jni.** com.code_intelligence.jazzer.bootstrap.@0 +rule kotlin.** com.code_intelligence.jazzer.bootstrap.@0 +rule net.sf.jsqlparser.** com.code_intelligence.jazzer.bootstrap.@0 +rule org.objectweb.asm.** com.code_intelligence.jazzer.bootstrap.@0 diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh b/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh new file mode 100755 index 00000000..b3a74ea9 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/runtime/verify_shading.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +# 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. + +[ -f "$1" ] || exit 1 +# List all files in the jar and exclude an allowed list of files. +# Since grep fails if there is no match, ! ... | grep ... fails if there is a +# match. +! external/local_jdk/bin/jar tf "$1" | \ + grep -v \ + -e '^com/$' \ + -e '^com/code_intelligence/$' \ + -e '^com/code_intelligence/jazzer/' \ + -e '^jaz/' \ + -e '^META-INF/$' \ + -e '^META-INF/MANIFEST.MF$' |