diff options
Diffstat (limited to 'src/test/java/com/code_intelligence/jazzer/instrumentor')
27 files changed, 2138 insertions, 0 deletions
diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java new file mode 100644 index 00000000..f8d6782c --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java @@ -0,0 +1,87 @@ +// 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.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +public class AfterHooks { + static AfterHooksTargetContract instance; + + @MethodHook(type = HookType.AFTER, + targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget", + targetMethod = "func1") + public static void + patchFunc1( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + instance = (AfterHooksTargetContract) thisObject; + ((AfterHooksTargetContract) thisObject).registerHasFunc1BeenCalled(); + } + + @MethodHook(type = HookType.AFTER, + targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget", + targetMethod = "registerTimesCalled", targetMethodDescriptor = "()V") + public static void + patchRegisterTimesCalled(MethodHandle method, Object thisObject, Object[] arguments, int hookId, + Object returnValue) throws Throwable { + // Invoke registerTimesCalled() again to pass the test. + method.invoke(); + } + + @MethodHook(type = HookType.AFTER, + targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget", + targetMethod = "getFirstSecret", targetMethodDescriptor = "()Ljava/lang/String;") + public static void + patchGetFirstSecret( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) { + // Use the returned secret to pass the test. + ((AfterHooksTargetContract) thisObject).verifyFirstSecret(returnValue); + } + + @MethodHook(type = HookType.AFTER, + targetClassName = "com.code_intelligence.jazzer.instrumentor.AfterHooksTarget", + targetMethod = "getSecondSecret") + public static void + patchGetSecondSecret( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + // Use the returned secret to pass the test. + ((AfterHooksTargetContract) thisObject).verifySecondSecret((String) returnValue); + } + + // Verify the interaction of a BEFORE and an AFTER hook. The BEFORE hook modifies the argument of + // the StringBuilder constructor. + @MethodHook( + type = HookType.BEFORE, targetClassName = "java.lang.StringBuilder", targetMethod = "<init>") + public static void + patchStringBuilderBeforeInit( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + arguments[0] = "hunter3"; + } + + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.StringBuilder", targetMethod = "<init>") + public static void + patchStringBuilderInit( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + String secret = ((StringBuilder) thisObject).toString(); + // Verify that the argument passed to this AFTER hook agrees with the argument passed to the + // StringBuilder constructor, which has been modified by the BEFORE hook. + if (secret.equals(arguments[0])) { + // Verify that the argument has been modified to the correct value "hunter3". + instance.verifyThirdSecret(secret); + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt new file mode 100644 index 00000000..55263786 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt @@ -0,0 +1,89 @@ +// 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.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode +import org.junit.Test +import java.io.File + +private fun getOriginalAfterHooksTargetInstance(): AfterHooksTargetContract { + return AfterHooksTarget() +} + +private fun getNoHooksAfterHooksTargetInstance(): AfterHooksTargetContract { + val originalBytecode = classToBytecode(AfterHooksTarget::class.java) + // Let the bytecode pass through the hooking logic, but don't apply any hooks. + val patchedBytecode = HookInstrumentor(emptyList(), false, null).instrument( + AfterHooksTarget::class.java.name.replace('.', '/'), + originalBytecode, + ) + val patchedClass = bytecodeToClass(AfterHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as AfterHooksTargetContract +} + +private fun getPatchedAfterHooksTargetInstance(classWithHooksEnabledField: Class<*>?): AfterHooksTargetContract { + val originalBytecode = classToBytecode(AfterHooksTarget::class.java) + val hooks = Hooks.loadHooks(emptyList(), setOf(AfterHooks::class.java.name)).first().hooks + val patchedBytecode = HookInstrumentor( + hooks, + false, + classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), + ).instrument(AfterHooksTarget::class.java.name.replace('.', '/'), originalBytecode) + // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. + val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") + File("$outDir/${AfterHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode) + File("$outDir/${AfterHooksTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode) + val patchedClass = bytecodeToClass(AfterHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as AfterHooksTargetContract +} + +class AfterHooksPatchTest { + + @Test + fun testOriginal() { + assertSelfCheck(getOriginalAfterHooksTargetInstance(), false) + } + + @Test + fun testPatchedWithoutHooks() { + assertSelfCheck(getNoHooksAfterHooksTargetInstance(), false) + } + + @Test + fun testPatched() { + assertSelfCheck(getPatchedAfterHooksTargetInstance(null), true) + } + + object HooksEnabled { + @Suppress("unused") + const val hooksEnabled = true + } + + object HooksDisabled { + @Suppress("unused") + const val hooksEnabled = false + } + + @Test + fun testPatchedWithConditionalHooksEnabled() { + assertSelfCheck(getPatchedAfterHooksTargetInstance(HooksEnabled::class.java), true) + } + + @Test + fun testPatchedWithConditionalHooksDisabled() { + assertSelfCheck(getPatchedAfterHooksTargetInstance(HooksDisabled::class.java), false) + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java new file mode 100644 index 00000000..a47b03a5 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java @@ -0,0 +1,85 @@ +// 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 java.util.HashMap; +import java.util.Map; + +// selfCheck() only passes with the hooks in AfterHooks.java applied. +public class AfterHooksTarget implements AfterHooksTargetContract { + static Map<String, Boolean> results = new HashMap<>(); + static int timesCalled = 0; + Boolean func1Called = false; + + public static void registerTimesCalled() { + timesCalled++; + results.put("hasBeenCalledTwice", timesCalled == 2); + } + + public Map<String, Boolean> selfCheck() { + results = new HashMap<>(); + + if (results.isEmpty()) { + registerHasFunc1BeenCalled(); + func1(); + } + + timesCalled = 0; + registerTimesCalled(); + + verifyFirstSecret("not_secret"); + getFirstSecret(); + + verifySecondSecret("not_secret_at_all"); + getSecondSecret(); + + verifyThirdSecret("not_the_secret"); + new StringBuilder("not_hunter3"); + + return results; + } + + public void func1() { + func1Called = true; + } + + public void registerHasFunc1BeenCalled() { + results.put("hasFunc1BeenCalled", func1Called); + } + + @SuppressWarnings("UnusedReturnValue") + String getFirstSecret() { + return "hunter2"; + } + + @SuppressWarnings("SameParameterValue") + public void verifyFirstSecret(String secret) { + results.put("verifyFirstSecret", secret.equals("hunter2")); + } + + @SuppressWarnings("UnusedReturnValue") + String getSecondSecret() { + return "hunter2!"; + } + + @SuppressWarnings("SameParameterValue") + public void verifySecondSecret(String secret) { + results.put("verifySecondSecret", secret.equals("hunter2!")); + } + + public void verifyThirdSecret(String secret) { + results.put("verifyThirdSecret", secret.equals("hunter3")); + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java new file mode 100644 index 00000000..cb12b148 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java @@ -0,0 +1,29 @@ +// 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; + +/** + * Helper interface used to call methods on instances of AfterHooksTarget classes loaded via + * different class loaders. + */ +public interface AfterHooksTargetContract extends DynamicTestContract { + void registerHasFunc1BeenCalled(); + + void verifyFirstSecret(String secret); + + void verifySecondSecret(String secret); + + void verifyThirdSecret(String secret); +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel new file mode 100644 index 00000000..4fdad567 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -0,0 +1,152 @@ +load("//bazel:kotlin.bzl", "ktlint", "wrapped_kt_jvm_test") +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "patch_test_utils", + srcs = [ + "DynamicTestContract.java", + "PatchTestUtils.kt", + ], + visibility = ["//visibility:public"], +) + +wrapped_kt_jvm_test( + name = "trace_data_flow_instrumentation_test", + size = "small", + srcs = [ + "MockTraceDataFlowCallbacks.java", + "TraceDataFlowInstrumentationTarget.java", + "TraceDataFlowInstrumentationTest.kt", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.TraceDataFlowInstrumentationTest", + deps = [ + ":patch_test_utils", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +wrapped_kt_jvm_test( + name = "coverage_instrumentation_test", + size = "small", + srcs = [ + "CoverageInstrumentationSpecialCasesTarget.java", + "CoverageInstrumentationTarget.java", + "CoverageInstrumentationTest.kt", + "MockCoverageMap.java", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.CoverageInstrumentationTest", + deps = [ + ":patch_test_utils", + "//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +wrapped_kt_jvm_test( + name = "descriptor_utils_test", + size = "small", + srcs = [ + "DescriptorUtilsTest.kt", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.DescriptorUtilsTest", + deps = [ + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +wrapped_kt_jvm_test( + name = "hook_validation_test", + size = "small", + srcs = [ + "HookValidationTest.kt", + "InvalidHookMocks.java", + "ValidHookMocks.java", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.HookValidationTest", + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +wrapped_kt_jvm_test( + name = "after_hooks_patch_test", + size = "small", + srcs = [ + "AfterHooks.java", + "AfterHooksPatchTest.kt", + "AfterHooksTarget.java", + "AfterHooksTargetContract.java", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.AfterHooksPatchTest", + deps = [ + ":patch_test_utils", + "//src/main/java/com/code_intelligence/jazzer/api", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +wrapped_kt_jvm_test( + name = "before_hooks_patch_test", + size = "small", + srcs = [ + "BeforeHooks.java", + "BeforeHooksPatchTest.kt", + "BeforeHooksTarget.java", + "BeforeHooksTargetContract.java", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.BeforeHooksPatchTest", + deps = [ + ":patch_test_utils", + "//src/main/java/com/code_intelligence/jazzer/api", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +wrapped_kt_jvm_test( + name = "replace_hooks_patch_test", + size = "small", + srcs = [ + "ReplaceHooks.java", + "ReplaceHooksInit.java", + "ReplaceHooksPatchTest.kt", + "ReplaceHooksTarget.java", + "ReplaceHooksTargetContract.java", + ], + associates = [ + "//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksPatchTest", + deps = [ + ":patch_test_utils", + "//src/main/java/com/code_intelligence/jazzer/api", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +ktlint() diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java new file mode 100644 index 00000000..31577dad --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java @@ -0,0 +1,53 @@ +// 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.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +public class BeforeHooks { + @MethodHook(type = HookType.BEFORE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.BeforeHooksTarget", + targetMethod = "hasFunc1BeenCalled", targetMethodDescriptor = "()Z") + public static void + patchHasFunc1BeenCalled(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + ((BeforeHooksTargetContract) thisObject).func1(); + } + + @MethodHook(type = HookType.BEFORE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.BeforeHooksTarget", + targetMethod = "getTimesCalled", targetMethodDescriptor = "()Ljava/lang/Integer;") + public static void + patchHasBeenCalled(MethodHandle method, Object thisObject, Object[] arguments, int hookId) + throws Throwable { + // Invoke static method getTimesCalled() again to pass the test. + method.invoke(); + } + + @MethodHook(type = HookType.BEFORE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.BeforeHooksTarget", + targetMethod = "hasFuncWithArgsBeenCalled") + public static void + patchHasFuncWithArgsBeenCalled( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length == 2 && arguments[0] instanceof Boolean + && arguments[1] instanceof String) { + // only if the arguments passed to the hook match the expected argument types and count invoke + // the method to pass the test + ((BeforeHooksTargetContract) thisObject).setFuncWithArgsCalled((Boolean) arguments[0]); + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt new file mode 100644 index 00000000..aae469c7 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt @@ -0,0 +1,89 @@ +// 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.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode +import org.junit.Test +import java.io.File + +private fun getOriginalBeforeHooksTargetInstance(): BeforeHooksTargetContract { + return BeforeHooksTarget() +} + +private fun getNoHooksBeforeHooksTargetInstance(): BeforeHooksTargetContract { + val originalBytecode = classToBytecode(BeforeHooksTarget::class.java) + // Let the bytecode pass through the hooking logic, but don't apply any hooks. + val patchedBytecode = HookInstrumentor(emptyList(), false, null).instrument( + BeforeHooksTarget::class.java.name.replace('.', '/'), + originalBytecode, + ) + val patchedClass = bytecodeToClass(BeforeHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as BeforeHooksTargetContract +} + +private fun getPatchedBeforeHooksTargetInstance(classWithHooksEnabledField: Class<*>?): BeforeHooksTargetContract { + val originalBytecode = classToBytecode(BeforeHooksTarget::class.java) + val hooks = Hooks.loadHooks(emptyList(), setOf(BeforeHooks::class.java.name)).first().hooks + val patchedBytecode = HookInstrumentor( + hooks, + false, + classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), + ).instrument(BeforeHooksTarget::class.java.name.replace('.', '/'), originalBytecode) + // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. + val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") + File("$outDir/${BeforeHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode) + File("$outDir/${BeforeHooksTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode) + val patchedClass = bytecodeToClass(BeforeHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as BeforeHooksTargetContract +} + +class BeforeHooksPatchTest { + + @Test + fun testOriginal() { + assertSelfCheck(getOriginalBeforeHooksTargetInstance(), false) + } + + @Test + fun testPatchedWithoutHooks() { + assertSelfCheck(getNoHooksBeforeHooksTargetInstance(), false) + } + + @Test + fun testPatched() { + assertSelfCheck(getPatchedBeforeHooksTargetInstance(null), true) + } + + object HooksEnabled { + @Suppress("unused") + const val hooksEnabled = true + } + + object HooksDisabled { + @Suppress("unused") + const val hooksEnabled = false + } + + @Test + fun testPatchedWithConditionalHooksEnabled() { + assertSelfCheck(getPatchedBeforeHooksTargetInstance(HooksEnabled::class.java), true) + } + + @Test + fun testPatchedWithConditionalHooksDisabled() { + assertSelfCheck(getPatchedBeforeHooksTargetInstance(HooksDisabled::class.java), false) + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java new file mode 100644 index 00000000..869e04bf --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java @@ -0,0 +1,61 @@ +// 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 java.util.HashMap; +import java.util.Map; + +// selfCheck() only passes with the hooks in BeforeHooks.java applied. +public class BeforeHooksTarget implements BeforeHooksTargetContract { + static private int timesCalled = 0; + Map<String, Boolean> results = new HashMap<>(); + Boolean func1Called = false; + Boolean funcWithArgsCalled = false; + + static Integer getTimesCalled() { + return ++timesCalled; + } + + public Map<String, Boolean> selfCheck() { + results = new HashMap<>(); + + results.put("hasFunc1BeenCalled", hasFunc1BeenCalled()); + + timesCalled = 0; + results.put("hasBeenCalledTwice", getTimesCalled() == 2); + + if (!results.containsKey("hasBeenCalledWithArgs")) { + results.put("hasBeenCalledWithArgs", hasFuncWithArgsBeenCalled(true, "foo")); + } + + return results; + } + + public void func1() { + func1Called = true; + } + + private boolean hasFunc1BeenCalled() { + return func1Called; + } + + public void setFuncWithArgsCalled(Boolean val) { + funcWithArgsCalled = val; + } + + private boolean hasFuncWithArgsBeenCalled(Boolean boolArgument, String stringArgument) { + return funcWithArgsCalled; + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java new file mode 100644 index 00000000..61f79dcc --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java @@ -0,0 +1,25 @@ +// 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; + +/** + * Helper interface used to call methods on instances of BeforeHooksTarget classes loaded via + * different class loaders. + */ +public interface BeforeHooksTargetContract extends DynamicTestContract { + void func1(); + + void setFuncWithArgsCalled(Boolean val); +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java new file mode 100644 index 00000000..cb811803 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java @@ -0,0 +1,41 @@ +// 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 java.util.Random; + +public class CoverageInstrumentationSpecialCasesTarget { + public ReturnClass newAfterJump() { + if (new Random().nextBoolean()) { + throw new RuntimeException(""); + } + return new ReturnClass(new Random().nextBoolean() ? "foo" : "bar"); + } + + public int newAndTryCatch() { + new Random(); + try { + new Random(); + return 2; + } catch (RuntimeException e) { + new Random(); + return 1; + } + } + + public static class ReturnClass { + public ReturnClass(String content) {} + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java new file mode 100644 index 00000000..7502481d --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java @@ -0,0 +1,67 @@ +// 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 java.util.HashMap; +import java.util.Map; + +public class CoverageInstrumentationTarget implements DynamicTestContract { + volatile int int1 = 3; + volatile int int2 = 213234; + + @Override + public Map<String, Boolean> selfCheck() { + HashMap<String, Boolean> results = new HashMap<>(); + + results.put("for0", false); + results.put("for1", false); + results.put("for2", false); + results.put("for3", false); + results.put("for4", false); + results.put("foobar", false); + results.put("baz", true); + + if (int1 < int2) { + results.put("block1", true); + } else { + results.put("block2", false); + } + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 5; j++) { + results.put("for" + j, i != 0); + } + } + + foo(results); + + return results; + } + + private void foo(HashMap<String, Boolean> results) { + bar(results); + } + + // The use of Map instead of HashMap is deliberate here: Since Map#put can throw exceptions, the + // invocation should be instrumented for coverage. + private void bar(Map<String, Boolean> results) { + results.put("foobar", true); + } + + @SuppressWarnings("unused") + private void baz(HashMap<String, Boolean> results) { + results.put("baz", false); + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt new file mode 100644 index 00000000..5a3c355a --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt @@ -0,0 +1,176 @@ +// 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.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 getOriginalInstrumentationTargetInstance(): DynamicTestContract { + return CoverageInstrumentationTarget() +} + +private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract { + val originalBytecode = classToBytecode(CoverageInstrumentationTarget::class.java) + val patchedBytecode = EdgeCoverageInstrumentor( + makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), + MockCoverageMap::class.java, + 0, + ).instrument(CoverageInstrumentationTarget::class.java.name.replace('.', '/'), originalBytecode) + // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. + val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") + File("$outDir/${CoverageInstrumentationTarget::class.java.simpleName}.class").writeBytes(originalBytecode) + File("$outDir/${CoverageInstrumentationTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode) + val patchedClass = bytecodeToClass(CoverageInstrumentationTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as DynamicTestContract +} + +private fun assertControlFlow(expectedLocations: List<Int>) { + assertEquals(expectedLocations, MockCoverageMap.locations.toList()) +} + +@Suppress("unused") +class CoverageInstrumentationTest { + + private val constructorReturn = 0 + + 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() { + assertSelfCheck(getOriginalInstrumentationTargetInstance()) + } + + @Test + fun testInstrumented() { + MockCoverageMap.clear() + assertSelfCheck(getInstrumentedInstrumentationTargetInstance()) + + 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(innerForCondition, innerForBodyIfFalseBranch, innerForBodyPutInvocation)) + } + add(outerForIncrementCounter) + }.toList() + val forSecondRunControlFlow = mutableListOf<Int>().apply { + add(outerForCondition) + repeat(5) { + addAll(listOf(innerForCondition, innerForBodyIfTrueBranch, innerForBodyPutInvocation)) + } + add(outerForIncrementCounter) + }.toList() + val forControlFlow = forFirstRunControlFlow + forSecondRunControlFlow + val fooCallControlFlow = listOf( + barAfterPutInvocation, + fooAfterBarInvocation, + afterFooInvocation, + ) + assertControlFlow( + listOf(constructorReturn) + + mapControlFlow + + ifControlFlow + + forControlFlow + + fooCallControlFlow, + ) + } + + @Test + fun testCounters() { + MockCoverageMap.clear() + + val target = getInstrumentedInstrumentationTargetInstance() + // The constructor of the target is run only once. + val takenOnceEdge = constructorReturn + // Control flows through the first if branch once per run. + val takenOnEveryRunEdge = ifTrueBranch + + var lastCounter = 0.toUByte() + for (i in 1..600) { + assertSelfCheck(target) + 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.counters[takenOnEveryRunEdge].toUByte() + assertEquals(expectedCounter, actualCounter, "After $i runs:") + } + } + + @Test + fun testSpecialCases() { + val originalBytecode = classToBytecode(CoverageInstrumentationSpecialCasesTarget::class.java) + val patchedBytecode = EdgeCoverageInstrumentor( + makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), + MockCoverageMap::class.java, + 0, + ).instrument(CoverageInstrumentationSpecialCasesTarget::class.java.name.replace('.', '/'), originalBytecode) + // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. + val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") + File("$outDir/${CoverageInstrumentationSpecialCasesTarget::class.simpleName}.class").writeBytes(originalBytecode) + File("$outDir/${CoverageInstrumentationSpecialCasesTarget::class.simpleName}.patched.class").writeBytes( + patchedBytecode, + ) + val patchedClass = bytecodeToClass(CoverageInstrumentationSpecialCasesTarget::class.java.name, patchedBytecode) + // Trigger a class load + patchedClass.declaredMethods + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt new file mode 100644 index 00000000..c1a3584e --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt @@ -0,0 +1,72 @@ +// 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 org.junit.Test +import kotlin.test.assertEquals + +class DescriptorUtilsTest { + + @Test + fun testClassDescriptor() { + assertEquals("V", java.lang.Void::class.javaPrimitiveType?.descriptor) + assertEquals("J", java.lang.Long::class.javaPrimitiveType?.descriptor) + assertEquals("[[[Z", Array<Array<BooleanArray>>::class.java.descriptor) + assertEquals("[Ljava/lang/String;", Array<String>::class.java.descriptor) + } + + @Test + fun testExtractInternalClassName() { + assertEquals("java/lang/String", extractInternalClassName("Ljava/lang/String;")) + assertEquals("[Ljava/lang/String;", extractInternalClassName("[Ljava/lang/String;")) + assertEquals("B", extractInternalClassName("B")) + } + + @Test + fun testExtractTypeDescriptors() { + val testCases = listOf( + Triple( + String::class.java.getMethod("equals", Object::class.java), + listOf("Ljava/lang/Object;"), + "Z", + ), + Triple( + String::class.java.getMethod("regionMatches", Boolean::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java, Int::class.javaPrimitiveType, Integer::class.javaPrimitiveType), + listOf("Z", "I", "Ljava/lang/String;", "I", "I"), + "Z", + ), + Triple( + String::class.java.getMethod("getChars", Integer::class.javaPrimitiveType, Int::class.javaPrimitiveType, CharArray::class.java, Int::class.javaPrimitiveType), + listOf("I", "I", "[C", "I"), + "V", + ), + Triple( + String::class.java.getMethod("subSequence", Integer::class.javaPrimitiveType, Integer::class.javaPrimitiveType), + listOf("I", "I"), + "Ljava/lang/CharSequence;", + ), + Triple( + String::class.java.getConstructor(), + emptyList(), + "V", + ), + ) + for ((executable, parameterDescriptors, returnTypeDescriptor) in testCases) { + val descriptor = executable.descriptor + assertEquals(extractParameterTypeDescriptors(descriptor), parameterDescriptors) + assertEquals(extractReturnTypeDescriptor(descriptor), returnTypeDescriptor) + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java new file mode 100644 index 00000000..163b226a --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java @@ -0,0 +1,21 @@ +// 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 java.util.Map; + +public interface DynamicTestContract { + Map<String, Boolean> selfCheck(); +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt new file mode 100644 index 00000000..bf02da72 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt @@ -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. + +package com.code_intelligence.jazzer.instrumentor + +import com.code_intelligence.jazzer.api.MethodHook +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class HookValidationTest { + @Test + fun testValidHooks() { + val hooks = Hooks.loadHooks(emptyList(), setOf(ValidHookMocks::class.java.name)).first().hooks + assertEquals(5, hooks.size) + } + + @Test + fun testInvalidHooks() { + for (method in InvalidHookMocks::class.java.methods) { + if (method.isAnnotationPresent(MethodHook::class.java)) { + assertFailsWith<IllegalArgumentException>("Expected ${method.name} to be an invalid hook") { + val methodHook = method.declaredAnnotations.first() as MethodHook + Hook.createAndVerifyHook(method, methodHook, methodHook.targetClassName) + } + } + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java new file mode 100644 index 00000000..0df349ca --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java @@ -0,0 +1,87 @@ +// 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.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( + MethodHandle method, String thisObject, Object[] arguments, long hookId) {} + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + private static void invalidAfterHook(MethodHandle method, String thisObject, Object[] arguments, + int hookId, Boolean returnValue) {} + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + public void invalidAfterHook2(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 String + incorrectReturnType(MethodHandle method, String thisObject, Object[] arguments, int hookId) { + return "foo"; + } + + @MethodHook( + type = HookType.REPLACE, targetClassName = "java.lang.String", targetMethod = "equals") + public static boolean + invalidReplaceHook2(MethodHandle method, Integer thisObject, Object[] arguments, int hookId) { + return true; + } + + @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) + throws Throwable { + return null; + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", + targetMethod = "startsWith", targetMethodDescriptor = "(Ljava/lang/String;)Z") + 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/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java new file mode 100644 index 00000000..3ea33d19 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java @@ -0,0 +1,53 @@ +// 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 java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +public class MockCoverageMap { + public static final int SIZE = 65536; + public static final ByteBuffer counters = ByteBuffer.allocate(SIZE); + + private static final ByteBuffer previous_mem = ByteBuffer.allocate(SIZE); + public static ArrayList<Integer> locations = new ArrayList<>(); + + public static void updated() { + int updated_pos = -1; + for (int i = 0; i < SIZE; i++) { + if (previous_mem.get(i) != counters.get(i)) { + updated_pos = i; + } + } + locations.add(updated_pos); + 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(counters.array(), (byte) 0); + Arrays.fill(previous_mem.array(), (byte) 0); + locations.clear(); + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java new file mode 100644 index 00000000..ad659da0 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java @@ -0,0 +1,106 @@ +// 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 java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public class MockTraceDataFlowCallbacks { + private static List<String> hookCalls; + private static int assertedCalls; + + public static void init() { + hookCalls = new ArrayList<>(); + assertedCalls = 0; + } + + public static boolean hookCall(String expectedCall) { + if (assertedCalls >= hookCalls.size()) { + System.err.println("Not seen (" + hookCalls.size() + " calls, but " + (assertedCalls + 1) + + " expected): " + expectedCall); + return false; + } + + if (!hookCalls.get(assertedCalls).equals(expectedCall)) { + System.err.println("Call " + expectedCall + " not seen, got " + hookCalls.get(assertedCalls)); + return false; + } + + assertedCalls++; + return true; + } + + public static boolean finish() { + if (assertedCalls == hookCalls.size()) + return true; + System.err.println("The following calls were not asserted:"); + for (int i = assertedCalls; i < hookCalls.size(); i++) { + System.err.println(hookCalls.get(i)); + } + + return false; + } + + public static void traceCmpLong(long arg1, long arg2, int pc) { + hookCalls.add("LCMP: " + Math.min(arg1, arg2) + ", " + Math.max(arg1, arg2)); + } + + public static void traceCmpInt(int arg1, int arg2, int pc) { + hookCalls.add("ICMP: " + Math.min(arg1, arg2) + ", " + Math.max(arg1, arg2)); + } + + public static void traceConstCmpInt(int arg1, int arg2, int pc) { + hookCalls.add("CICMP: " + arg1 + ", " + arg2); + } + + public static void traceDivInt(int val, int pc) { + hookCalls.add("IDIV: " + val); + } + + public static void traceDivLong(long val, int pc) { + hookCalls.add("LDIV: " + val); + } + + public static void traceGep(long idx, int pc) { + hookCalls.add("GEP: " + idx); + } + + public static void traceSwitch(long switchValue, long[] libfuzzerCaseValues, int pc) { + if (libfuzzerCaseValues.length < 3 + // number of case values must match length + || libfuzzerCaseValues[0] != libfuzzerCaseValues.length - 2 + // bit size of case values is always 32 (int) + || libfuzzerCaseValues[1] != 32) { + hookCalls.add("INVALID_SWITCH"); + return; + } + + StringBuilder builder = new StringBuilder("SWITCH: " + switchValue + ", ("); + for (int i = 2; i < libfuzzerCaseValues.length; i++) { + builder.append(libfuzzerCaseValues[i]); + builder.append(", "); + } + builder.append(")"); + hookCalls.add(builder.toString()); + } + + public static int traceCmpLongWrapper(long value1, long value2, int pc) { + traceCmpLong(value1, value2, pc); + // Long.compare serves as a substitute for the lcmp opcode here + // (behaviour is the same) + return Long.compare(value1, value2); + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt new file mode 100644 index 00000000..de2cc187 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt @@ -0,0 +1,64 @@ +// 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 java.io.FileOutputStream + +object PatchTestUtils { + @JvmStatic + fun classToBytecode(targetClass: Class<*>): ByteArray { + return ClassLoader + .getSystemClassLoader() + .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!! + .use { + it.readBytes() + } + } + + @JvmStatic + fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> { + return BytecodeClassLoader(name, bytecode).loadClass(name) + } + + @JvmStatic + fun dumpBytecode(outDir: String, name: String, originalBytecode: ByteArray) { + FileOutputStream("$outDir/$name.class").use { fos -> fos.write(originalBytecode) } + } + + /** + * 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) + } + } +} + +fun assertSelfCheck(target: DynamicTestContract, shouldPass: Boolean = true) { + val results = target.selfCheck() + for ((test, passed) in results) { + if (shouldPass) { + assert(passed) { "$test should pass" } + } else { + assert(!passed) { "$test should not pass" } + } + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java new file mode 100644 index 00000000..7e31b77b --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java @@ -0,0 +1,136 @@ +// 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.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", + targetMethod = "shouldReturnTrue1") + public static boolean + patchShouldReturnTrue1(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return true; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldReturnTrue2") + public static Boolean + patchShouldReturnTrue2(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return true; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldReturnTrue3") + public static Object + patchShouldReturnTrue3(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return true; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldReturnFalse1") + public static Boolean + patchShouldReturnFalse1(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return false; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldReturnFalse2") + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldReturnFalse3") + public static Object + patchShouldReturnFalse2(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return false; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldReturnReversed", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/String;") + public static String + patchShouldReturnReversed( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return new StringBuilder((String) arguments[0]).reverse().toString(); + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldIncrement") + public static int + patchShouldIncrement(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return ((int) arguments[0]) + 1; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "shouldCallPass") + public static void + patchShouldCallPass(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + ((ReplaceHooksTargetContract) thisObject).pass("shouldCallPass"); + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", + targetMethod = "idempotent", targetMethodDescriptor = "(I)I") + public static int + patchIdempotent(MethodHandle method, Object thisObject, Object[] arguments, int hookId) + throws Throwable { + // Iterate the function twice to pass the test. + int input = (int) arguments[0]; + int temp = (int) method.invokeWithArguments(thisObject, input); + return (int) method.invokeWithArguments(thisObject, temp); + } + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.util.AbstractList", + targetMethod = "get", targetMethodDescriptor = "(I)Ljava/lang/Object;") + public static Object + 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/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java new file mode 100644 index 00000000..da77be81 --- /dev/null +++ b/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/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt new file mode 100644 index 00000000..275c43f9 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt @@ -0,0 +1,89 @@ +// 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.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode +import org.junit.Test +import java.io.File + +private fun getOriginalReplaceHooksTargetInstance(): ReplaceHooksTargetContract { + return ReplaceHooksTarget() +} + +private fun getNoHooksReplaceHooksTargetInstance(): ReplaceHooksTargetContract { + val originalBytecode = classToBytecode(ReplaceHooksTarget::class.java) + // Let the bytecode pass through the hooking logic, but don't apply any hooks. + val patchedBytecode = HookInstrumentor(emptyList(), false, null).instrument( + ReplaceHooksTarget::class.java.name.replace('.', '/'), + originalBytecode, + ) + val patchedClass = bytecodeToClass(ReplaceHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as ReplaceHooksTargetContract +} + +private fun getPatchedReplaceHooksTargetInstance(classWithHooksEnabledField: Class<*>?): ReplaceHooksTargetContract { + val originalBytecode = classToBytecode(ReplaceHooksTarget::class.java) + val hooks = Hooks.loadHooks(emptyList(), setOf(ReplaceHooks::class.java.name)).first().hooks + val patchedBytecode = HookInstrumentor( + hooks, + false, + classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), + ).instrument(ReplaceHooksTarget::class.java.name.replace('.', '/'), originalBytecode) + // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. + val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") + File("$outDir/${ReplaceHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode) + File("$outDir/${ReplaceHooksTarget::class.java.simpleName}.patched.class").writeBytes(patchedBytecode) + val patchedClass = bytecodeToClass(ReplaceHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as ReplaceHooksTargetContract +} + +class ReplaceHooksPatchTest { + + @Test + fun testOriginal() { + assertSelfCheck(getOriginalReplaceHooksTargetInstance(), false) + } + + @Test + fun testPatchedWithoutHooks() { + assertSelfCheck(getNoHooksReplaceHooksTargetInstance(), false) + } + + @Test + fun testPatched() { + assertSelfCheck(getPatchedReplaceHooksTargetInstance(null), true) + } + + object HooksEnabled { + @Suppress("unused") + const val hooksEnabled = true + } + + object HooksDisabled { + @Suppress("unused") + const val hooksEnabled = false + } + + @Test + fun testPatchedWithConditionalHooksEnabled() { + assertSelfCheck(getPatchedReplaceHooksTargetInstance(HooksEnabled::class.java), true) + } + + @Test + fun testPatchedWithConditionalHooksDisabled() { + assertSelfCheck(getPatchedReplaceHooksTargetInstance(HooksDisabled::class.java), false) + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java new file mode 100644 index 00000000..fadbdf80 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java @@ -0,0 +1,126 @@ +// 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 java.security.SecureRandom; +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. +public class ReplaceHooksTarget implements ReplaceHooksTargetContract { + Map<String, Boolean> results = new HashMap<>(); + + public static boolean shouldReturnTrue3() { + // return true; + return false; + } + + public Map<String, Boolean> selfCheck() { + results = new HashMap<>(); + + results.put("shouldReturnTrue1", shouldReturnTrue1()); + results.put("shouldReturnTrue2", shouldReturnTrue2()); + results.put("shouldReturnTrue3", shouldReturnTrue3()); + try { + boolean notTrue = false; + results.put("shouldReturnFalse1", notTrue); + if (!results.get("shouldReturnFalse1")) + results.put("shouldReturnFalse1", !shouldReturnFalse1()); + boolean notFalse = true; + results.put("shouldReturnFalse2", !shouldReturnFalse2() && notFalse); + results.put("shouldReturnFalse3", !shouldReturnFalse3()); + } catch (Exception e) { + boolean notTrue = false; + results.put("shouldNotBeExecuted", notTrue); + } + results.put("shouldReturnReversed", shouldReturnReversed("foo").equals("oof")); + results.put("shouldIncrement", shouldIncrement(5) == 6); + results.put("verifyIdentity", verifyIdentity()); + + results.put("shouldCallPass", false); + if (!results.get("shouldCallPass")) { + shouldCallPass(); + } + + 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; + } + + public boolean shouldReturnTrue1() { + // return true; + return false; + } + + public boolean shouldReturnTrue2() { + // return true; + return false; + } + + protected Boolean shouldReturnFalse1() { + // return false; + return true; + } + + Boolean shouldReturnFalse2() { + // return false; + return true; + } + + public Boolean shouldReturnFalse3() { + // return false; + return true; + } + + public String shouldReturnReversed(String input) { + // return new StringBuilder(input).reverse().toString(); + return input; + } + + public int shouldIncrement(int input) { + // return input + 1; + return input; + } + + private void shouldCallPass() { + // pass("shouldCallPass"); + } + + private boolean verifyIdentity() { + SecureRandom rand = new SecureRandom(); + int input = rand.nextInt(); + // return idempotent(idempotent(input)) == input; + return idempotent(input) == input; + } + + private int idempotent(int input) { + int secret = 0x12345678; + return input ^ secret; + } + + public void pass(String test) { + results.put(test, true); + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java new file mode 100644 index 00000000..e3dff93e --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java @@ -0,0 +1,23 @@ +// 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; + +/** + * Helper interface used to call methods on instances of ReplaceHooksTarget classes loaded via + * different class loaders. + */ +public interface ReplaceHooksTargetContract extends DynamicTestContract { + void pass(String test); +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java new file mode 100644 index 00000000..d8e28881 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java @@ -0,0 +1,153 @@ +// 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 java.nio.ByteBuffer; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.Vector; + +public class TraceDataFlowInstrumentationTarget implements DynamicTestContract { + volatile long long1 = 1; + volatile long long2 = 1; + volatile long long3 = 2; + volatile long long4 = 3; + + volatile int int1 = 4; + volatile int int2 = 4; + volatile int int3 = 6; + volatile int int4 = 5; + + volatile int switchValue = 1200; + + @SuppressWarnings("ReturnValueIgnored") + @Override + public Map<String, Boolean> selfCheck() { + Map<String, Boolean> results = new HashMap<>(); + + results.put("longCompareEq", long1 == long2); + results.put("longCompareNe", long3 != long4); + + results.put("intCompareEq", int1 == int2); + results.put("intCompareNe", int3 != int4); + results.put("intCompareLt", int4 < int3); + results.put("intCompareLe", int4 <= int3); + results.put("intCompareGt", int3 > int4); + results.put("intCompareGe", int3 >= int4); + + // Not instrumented since all case values are non-negative and < 256. + switch (switchValue) { + case 119: + case 120: + case 121: + results.put("tableSwitchUninstrumented", false); + break; + default: + results.put("tableSwitchUninstrumented", true); + } + + // Not instrumented since all case values are non-negative and < 256. + switch (switchValue) { + case 1: + case 200: + results.put("lookupSwitchUninstrumented", false); + break; + default: + results.put("lookupSwitchUninstrumented", true); + } + + results.put("emptySwitchUninstrumented", false); + switch (switchValue) { + default: + results.put("emptySwitchUninstrumented", true); + } + + switch (switchValue) { + case 1000: + case 1001: + // case 1002: The tableswitch instruction will contain a gap case for 1002. + case 1003: + results.put("tableSwitch", false); + break; + default: + results.put("tableSwitch", true); + } + + switch (-switchValue) { + case -1200: + results.put("lookupSwitch", true); + break; + case -1: + case -10: + case -1000: + case 200: + default: + results.put("lookupSwitch", false); + } + + results.put("intDiv", (int3 / 2) == 3); + + results.put("longDiv", (long4 / 2) == 1); + + String[] referenceArray = {"foo", "foo", "bar"}; + boolean[] boolArray = {false, false, true}; + byte[] byteArray = {0, 0, 2}; + char[] charArray = {0, 0, 0, 3}; + double[] doubleArray = {0, 0, 0, 0, 4}; + float[] floatArray = {0, 0, 0, 0, 0, 5}; + int[] intArray = {0, 0, 0, 0, 0, 0, 6}; + long[] longArray = {0, 0, 0, 0, 0, 0, 0, 7}; + short[] shortArray = {0, 0, 0, 0, 0, 0, 0, 0, 8}; + + results.put("referenceArrayGep", referenceArray[2].equals("bar")); + results.put("boolArrayGep", boolArray[2]); + results.put("byteArrayGep", byteArray[2] == 2); + results.put("charArrayGep", charArray[3] == 3); + results.put("doubleArrayGep", doubleArray[4] == 4); + results.put("floatArrayGep", floatArray[5] == 5); + results.put("intArrayGep", intArray[6] == 6); + results.put("longArrayGep", longArray[7] == 7); + results.put("shortArrayGep", shortArray[8] == 8); + + ByteBuffer buffer = ByteBuffer.allocate(100); + buffer.get(2); + buffer.getChar(3); + buffer.getDouble(4); + buffer.getFloat(5); + buffer.getInt(6); + buffer.getLong(7); + buffer.getShort(8); + + "foobarbazbat".charAt(9); + "foobarbazbat".codePointAt(10); + new StringBuilder("foobarbazbat").charAt(11); + + (new Vector<>(Collections.nCopies(20, "foo"))).get(12); + (new ArrayList<>(Collections.nCopies(20, "foo"))).get(13); + Stack<String> stack = new Stack<>(); + for (int i = 0; i < 20; i++) stack.push("foo"); + stack.get(14); + stack.get(15); + ((AbstractList<String>) stack).get(16); + ((List<String>) stack).get(17); + + return results; + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt new file mode 100644 index 00000000..b7383f1f --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt @@ -0,0 +1,143 @@ +// 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.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode +import org.junit.Test +import java.io.File + +private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract { + return TraceDataFlowInstrumentationTarget() +} + +private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract { + val originalBytecode = classToBytecode(TraceDataFlowInstrumentationTarget::class.java) + val patchedBytecode = TraceDataFlowInstrumentor( + setOf( + InstrumentationType.CMP, + InstrumentationType.DIV, + InstrumentationType.GEP, + ), + MockTraceDataFlowCallbacks::class.java.name.replace('.', '/'), + ).instrument(TraceDataFlowInstrumentationTarget::class.java.name.replace('.', '/'), originalBytecode) + // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. + val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") + File("$outDir/${TraceDataFlowInstrumentationTarget::class.simpleName}.class").writeBytes(originalBytecode) + File("$outDir/${TraceDataFlowInstrumentationTarget::class.simpleName}.patched.class").writeBytes(patchedBytecode) + val patchedClass = bytecodeToClass(TraceDataFlowInstrumentationTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as DynamicTestContract +} + +class TraceDataFlowInstrumentationTest { + + @Test + fun testOriginal() { + MockTraceDataFlowCallbacks.init() + assertSelfCheck(getOriginalInstrumentationTargetInstance()) + assert(MockTraceDataFlowCallbacks.finish()) + } + + @Test + fun testInstrumented() { + MockTraceDataFlowCallbacks.init() + assertSelfCheck(getInstrumentedInstrumentationTargetInstance()) + listOf( + // long compares + "LCMP: 1, 1", + "LCMP: 2, 3", + // int compares + "ICMP: 4, 4", + "ICMP: 5, 6", + "ICMP: 5, 6", + "ICMP: 5, 6", + "ICMP: 5, 6", + "ICMP: 5, 6", + // tableswitch with gap + "SWITCH: 1200, (1000, 1001, 1003, )", + // lookupswitch + "SWITCH: -1200, (200, -1200, -1000, -10, -1, )", + // (6 / 2) == 3 + "IDIV: 2", + "ICMP: 3, 3", + // (3 / 2) == 1 + "LDIV: 2", + "LCMP: 1, 1", + // referenceArray[2] + "GEP: 2", + // boolArray[2] + "GEP: 2", + // byteArray[2] == 2 + "GEP: 2", + "ICMP: 2, 2", + // charArray[3] == 3 + "GEP: 3", + "ICMP: 3, 3", + // doubleArray[4] == 4 + "GEP: 4", + // floatArray[5] == 5 + "GEP: 5", + "CICMP: 0, 0", + // intArray[6] == 6 + "GEP: 6", + "ICMP: 6, 6", + // longArray[7] == 7 + "GEP: 7", + "LCMP: 7, 7", + // shortArray[8] == 8 + "GEP: 8", + "ICMP: 8, 8", + + "GEP: 2", + "GEP: 3", + "GEP: 4", + "GEP: 5", + "GEP: 6", + "GEP: 7", + "GEP: 8", + "GEP: 9", + "GEP: 10", + "GEP: 11", + "GEP: 12", + "GEP: 13", + "ICMP: 0, 20", + "ICMP: 1, 20", + "ICMP: 2, 20", + "ICMP: 3, 20", + "ICMP: 4, 20", + "ICMP: 5, 20", + "ICMP: 6, 20", + "ICMP: 7, 20", + "ICMP: 8, 20", + "ICMP: 9, 20", + "ICMP: 10, 20", + "ICMP: 11, 20", + "ICMP: 12, 20", + "ICMP: 13, 20", + "ICMP: 14, 20", + "ICMP: 15, 20", + "ICMP: 16, 20", + "ICMP: 17, 20", + "ICMP: 18, 20", + "ICMP: 19, 20", + "ICMP: 20, 20", + "GEP: 14", + "GEP: 15", + "GEP: 16", + "GEP: 17", + ).forEach { assert(MockTraceDataFlowCallbacks.hookCall(it)) } + assert(MockTraceDataFlowCallbacks.finish()) + } +} diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java b/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java new file mode 100644 index 00000000..a919242b --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java @@ -0,0 +1,45 @@ +// 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.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +class ValidHookMocks { + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.String", targetMethod = "equals") + public static void validBeforeHook( + MethodHandle method, String thisObject, Object[] arguments, int hookId) {} + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + public static void validAfterHook(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 + validReplaceHook(MethodHandle method, String thisObject, Object[] arguments, int hookId) { + return true; + } + + @MethodHook( + type = HookType.REPLACE, targetClassName = "java.lang.String", targetMethod = "equals") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.String", + targetMethod = "equalsIgnoreCase") + public static boolean + validReplaceHook2(MethodHandle method, String thisObject, Object[] arguments, int hookId) { + return true; + } +} |