diff options
Diffstat (limited to 'agent/src/test/java/com/code_intelligence/jazzer')
36 files changed, 2667 insertions, 0 deletions
diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java new file mode 100644 index 00000000..66a85db6 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java @@ -0,0 +1,107 @@ +// 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.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; + +public class AutofuzzTest { + public interface UnimplementedInterface {} + + public interface ImplementedInterface {} + public static class ImplementingClass implements ImplementedInterface {} + + private static boolean implIsNotNull(ImplementedInterface impl) { + return impl != null; + } + + private static boolean implIsNotNull(UnimplementedInterface impl) { + return impl != null; + } + + private static void checkAllTheArguments( + String arg1, int arg2, byte arg3, ImplementedInterface arg4) { + if (!arg1.equals("foobar") || arg2 != 42 || arg3 != 5 || arg4 == null) { + throw new IllegalArgumentException(); + } + } + + @Test + public void testConsume() { + FuzzedDataProvider data = CannedFuzzedDataProvider.create( + Arrays.asList((byte) 1 /* do not return null */, 0 /* first class on the classpath */, + (byte) 1 /* do not return null */, 0 /* first constructor */)); + ImplementedInterface result = Jazzer.consume(data, ImplementedInterface.class); + assertNotNull(result); + } + + @Test + public void testConsumeFailsWithoutException() { + FuzzedDataProvider data = CannedFuzzedDataProvider.create(Collections.singletonList( + (byte) 1 /* do not return null without searching for implementing classes */)); + assertNull(Jazzer.consume(data, UnimplementedInterface.class)); + } + + @Test + public void testAutofuzz() { + FuzzedDataProvider data = CannedFuzzedDataProvider.create( + Arrays.asList((byte) 1 /* do not return null */, 0 /* first class on the classpath */, + (byte) 1 /* do not return null */, 0 /* first constructor */)); + assertEquals(Boolean.TRUE, + Jazzer.autofuzz(data, (Function1<ImplementedInterface, ?>) AutofuzzTest::implIsNotNull)); + } + + @Test + public void testAutofuzzFailsWithException() { + FuzzedDataProvider data = CannedFuzzedDataProvider.create( + Collections.singletonList((byte) 1 /* do not return null */)); + try { + Jazzer.autofuzz(data, (Function1<UnimplementedInterface, ?>) AutofuzzTest::implIsNotNull); + } catch (AutofuzzConstructionException e) { + // Pass. + return; + } + fail("should have thrown an AutofuzzConstructionException"); + } + + @Test + public void testAutofuzzConsumer() { + FuzzedDataProvider data = CannedFuzzedDataProvider.create( + Arrays.asList((byte) 1 /* do not return null */, 6 /* string length */, "foobar", 42, + (byte) 5, (byte) 1 /* do not return null */, 0 /* first class on the classpath */, + (byte) 1 /* do not return null */, 0 /* first constructor */)); + Jazzer.autofuzz(data, AutofuzzTest::checkAllTheArguments); + } + + @Test + public void testAutofuzzConsumerThrowsException() { + FuzzedDataProvider data = + CannedFuzzedDataProvider.create(Arrays.asList((byte) 1 /* do not return null */, + 6 /* string length */, "foobar", 42, (byte) 5, (byte) 0 /* *do* return null */)); + try { + Jazzer.autofuzz(data, AutofuzzTest::checkAllTheArguments); + } catch (IllegalArgumentException e) { + // Pass. + return; + } + fail("should have thrown an IllegalArgumentException"); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel new file mode 100644 index 00000000..9192ff77 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -0,0 +1,21 @@ +java_test( + name = "AutofuzzTest", + size = "small", + srcs = [ + "AutofuzzTest.java", + ], + env = { + # Also consider implementing classes from com.code_intelligence.jazzer.*. + "JAZZER_AUTOFUZZ_TESTING": "1", + }, + test_class = "com.code_intelligence.jazzer.api.AutofuzzTest", + runtime_deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz", + # Needed for JazzerInternal. + "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "@maven//:junit_junit", + ], +) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel new file mode 100644 index 00000000..f8448f01 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BUILD.bazel @@ -0,0 +1,69 @@ +java_test( + name = "MetaTest", + size = "small", + srcs = [ + "MetaTest.java", + ], + test_class = "com.code_intelligence.jazzer.autofuzz.MetaTest", + deps = [ + ":test_helpers", + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz", + "@maven//:com_mikesamuel_json_sanitizer", + "@maven//:junit_junit", + ], +) + +java_test( + name = "InterfaceCreationTest", + size = "small", + srcs = [ + "InterfaceCreationTest.java", + ], + env = { + # Also consider implementing classes from com.code_intelligence.jazzer.*. + "JAZZER_AUTOFUZZ_TESTING": "1", + }, + test_class = "com.code_intelligence.jazzer.autofuzz.InterfaceCreationTest", + deps = [ + ":test_helpers", + "@maven//:junit_junit", + ], +) + +java_test( + name = "BuilderPatternTest", + size = "small", + srcs = [ + "BuilderPatternTest.java", + ], + test_class = "com.code_intelligence.jazzer.autofuzz.BuilderPatternTest", + deps = [ + ":test_helpers", + "@maven//:junit_junit", + ], +) + +java_test( + name = "SettersTest", + size = "small", + srcs = [ + "SettersTest.java", + ], + test_class = "com.code_intelligence.jazzer.autofuzz.SettersTest", + deps = [ + ":test_helpers", + "//agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata:test_data", + "@maven//:junit_junit", + ], +) + +java_library( + name = "test_helpers", + srcs = ["TestHelpers.java"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz", + "@maven//:junit_junit", + ], +) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BuilderPatternTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BuilderPatternTest.java new file mode 100644 index 00000000..a602d712 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/BuilderPatternTest.java @@ -0,0 +1,103 @@ +// 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.autofuzz; + +import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase; + +import java.util.Arrays; +import java.util.Objects; +import org.junit.Test; + +class Employee { + private final String firstName; + private final String lastName; + private final String jobTitle; + private final int age; + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Employee hero = (Employee) o; + return age == hero.age && Objects.equals(firstName, hero.firstName) + && Objects.equals(lastName, hero.lastName) && Objects.equals(jobTitle, hero.jobTitle); + } + + @Override + public int hashCode() { + return Objects.hash(firstName, lastName, jobTitle, age); + } + + private Employee(Builder builder) { + this.jobTitle = builder.jobTitle; + this.firstName = builder.firstName; + this.lastName = builder.lastName; + this.age = builder.age; + } + + public static class Builder { + private final String firstName; + private final String lastName; + private String jobTitle; + private int age; + + public Builder(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public Builder withAge(int age) { + this.age = age; + return this; + } + + public Builder withJobTitle(String jobTitle) { + this.jobTitle = jobTitle; + return this; + } + + public Employee build() { + return new Employee(this); + } + } +} + +public class BuilderPatternTest { + @Test + public void testBuilderPattern() { + consumeTestCase(new Employee.Builder("foo", "bar").withAge(20).withJobTitle("baz").build(), + "new com.code_intelligence.jazzer.autofuzz.Employee.Builder(\"foo\", \"bar\").withAge(20).withJobTitle(\"baz\").build()", + Arrays.asList((byte) 1, // do not return null + 0, // Select the first Builder + 2, // Select two Builder methods returning a builder object (fluent design) + 0, // Select the first build method + 0, // pick the first remaining builder method (withAge) + 0, // pick the first remaining builder method (withJobTitle) + 0, // pick the first build method + (byte) 1, // do not return null + 6, // remaining bytes + "foo", // firstName + (byte) 1, // do not return null + 6, // remaining bytes + "bar", // lastName + 20, // age + (byte) 1, // do not return null + 6, // remaining bytes + "baz" // jobTitle + )); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/InterfaceCreationTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/InterfaceCreationTest.java new file mode 100644 index 00000000..4d85ca6c --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/InterfaceCreationTest.java @@ -0,0 +1,111 @@ +// 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.autofuzz; + +import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase; + +import java.util.Arrays; +import java.util.Objects; +import org.junit.Test; + +interface InterfaceA { + void foo(); + + void bar(); +} + +abstract class ClassA1 implements InterfaceA { + @Override + public void foo() {} +} + +class ClassB1 extends ClassA1 { + int n; + + public ClassB1(int _n) { + n = _n; + } + + @Override + public void bar() {} + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ClassB1 classB1 = (ClassB1) o; + return n == classB1.n; + } + + @Override + public int hashCode() { + return Objects.hash(n); + } +} + +class ClassB2 implements InterfaceA { + String s; + + public ClassB2(String _s) { + s = _s; + } + + @Override + public void foo() {} + + @Override + public void bar() {} + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ClassB2 classB2 = (ClassB2) o; + return Objects.equals(s, classB2.s); + } + + @Override + public int hashCode() { + return Objects.hash(s); + } +} + +public class InterfaceCreationTest { + @Test + public void testConsumeInterface() { + consumeTestCase(InterfaceA.class, new ClassB1(5), + "(com.code_intelligence.jazzer.autofuzz.InterfaceA) new com.code_intelligence.jazzer.autofuzz.ClassB1(5)", + Arrays.asList((byte) 1, // do not return null + 0, // pick ClassB1 + (byte) 1, // do not return null + 0, // pick first constructor + 5 // arg for ClassB1 constructor + )); + consumeTestCase(InterfaceA.class, new ClassB2("test"), + "(com.code_intelligence.jazzer.autofuzz.InterfaceA) new com.code_intelligence.jazzer.autofuzz.ClassB2(\"test\")", + Arrays.asList((byte) 1, // do not return null + 1, // pick ClassB2 + (byte) 1, // do not return null + 0, // pick first constructor + (byte) 1, // do not return null + 8, // remaining bytes + "test" // arg for ClassB2 constructor + )); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java new file mode 100644 index 00000000..0615e9ae --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java @@ -0,0 +1,147 @@ +// 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.autofuzz; + +import static com.code_intelligence.jazzer.autofuzz.TestHelpers.autofuzzTestCase; +import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase; +import static org.junit.Assert.assertEquals; + +import com.code_intelligence.jazzer.api.CannedFuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.google.json.JsonSanitizer; +import java.io.ByteArrayInputStream; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; + +public class MetaTest { + public static boolean isFive(int arg) { + return arg == 5; + } + + public static boolean intEquals(int arg1, int arg2) { + return arg1 == arg2; + } + + public enum TestEnum { + FOO, + BAR, + BAZ, + } + + @Test + public void testConsume() { + consumeTestCase(5, "5", Collections.singletonList(5)); + consumeTestCase((short) 5, "(short) 5", Collections.singletonList((short) 5)); + consumeTestCase(5L, "5L", Collections.singletonList(5L)); + consumeTestCase(5.0F, "5.0F", Collections.singletonList(5.0F)); + consumeTestCase('\n', "'\\\\n'", Collections.singletonList('\n')); + consumeTestCase('\'', "'\\\\''", Collections.singletonList('\'')); + consumeTestCase('\\', "'\\\\'", Collections.singletonList('\\')); + + String testString = "foo\n\t\\\"bar"; + // The expected string is obtained from testString by escaping, wrapping into quotes and + // escaping again. + consumeTestCase(testString, "\"foo\\\\n\\\\t\\\\\\\\\"bar\"", + Arrays.asList((byte) 1, // do not return null + testString.length(), testString)); + + consumeTestCase(null, "null", Collections.singletonList((byte) 0)); + + boolean[] testBooleans = new boolean[] {true, false, true}; + consumeTestCase(testBooleans, "new boolean[]{true, false, true}", + Arrays.asList((byte) 1, // do not return null for the array + 2 * 3, testBooleans)); + + char[] testChars = new char[] {'a', '\n', '\''}; + consumeTestCase(testChars, "new char[]{'a', '\\\\n', '\\\\''}", + Arrays.asList((byte) 1, // do not return null for the array + 2 * 3 * Character.BYTES + Character.BYTES, testChars[0], 2 * 3 * Character.BYTES, + 2 * 3 * Character.BYTES, // remaining bytes, 2 times what is needed for 3 chars + testChars[1], testChars[2])); + + char[] testNoChars = new char[] {}; + consumeTestCase(testNoChars, "new char[]{}", + Arrays.asList((byte) 1, // do not return null for the array + 0, 'a', 0, 0)); + + short[] testShorts = new short[] {(short) 1, (short) 2, (short) 3}; + consumeTestCase(testShorts, "new short[]{(short) 1, (short) 2, (short) 3}", + Arrays.asList((byte) 1, // do not return null for the array + 2 * 3 * Short.BYTES, // remaining bytes + testShorts)); + + long[] testLongs = new long[] {1L, 2L, 3L}; + consumeTestCase(testLongs, "new long[]{1L, 2L, 3L}", + Arrays.asList((byte) 1, // do not return null for the array + 2 * 3 * Long.BYTES, // remaining bytes + testLongs)); + + consumeTestCase(new String[] {"foo", "bar", "foo\nbar"}, + "new java.lang.String[]{\"foo\", \"bar\", \"foo\\\\nbar\"}", + Arrays.asList((byte) 1, // do not return null for the array + 32, // remaining bytes + (byte) 1, // do not return null for the string + 31, // remaining bytes + "foo", + 28, // remaining bytes + 28, // array length + (byte) 1, // do not return null for the string + 27, // remaining bytes + "bar", + (byte) 1, // do not return null for the string + 23, // remaining bytes + "foo\nbar")); + + byte[] testInputStreamBytes = new byte[] {(byte) 1, (byte) 2, (byte) 3}; + consumeTestCase(new ByteArrayInputStream(testInputStreamBytes), + "new java.io.ByteArrayInputStream(new byte[]{(byte) 1, (byte) 2, (byte) 3})", + Arrays.asList((byte) 1, // do not return null for the InputStream + 2 * 3, // remaining bytes (twice the desired length) + testInputStreamBytes)); + + consumeTestCase(TestEnum.BAR, + String.format("%s.%s", TestEnum.class.getName(), TestEnum.BAR.name()), + Arrays.asList((byte) 1, // do not return null for the enum value + 1 /* second value */ + )); + + consumeTestCase(YourAverageJavaClass.class, + "com.code_intelligence.jazzer.autofuzz.YourAverageJavaClass.class", + Collections.singletonList((byte) 1)); + } + + @Test + public void testAutofuzz() throws NoSuchMethodException { + autofuzzTestCase(true, "com.code_intelligence.jazzer.autofuzz.MetaTest.isFive(5)", + MetaTest.class.getMethod("isFive", int.class), Collections.singletonList(5)); + autofuzzTestCase(false, "com.code_intelligence.jazzer.autofuzz.MetaTest.intEquals(5, 4)", + MetaTest.class.getMethod("intEquals", int.class, int.class), Arrays.asList(5, 4)); + autofuzzTestCase("foobar", "\"foo\".concat(\"bar\")", + String.class.getMethod("concat", String.class), + Arrays.asList((byte) 1, 6, "foo", (byte) 1, 6, "bar")); + autofuzzTestCase("jazzer", "new java.lang.String(\"jazzer\")", + String.class.getConstructor(String.class), Arrays.asList((byte) 1, 12, "jazzer")); + autofuzzTestCase("\"jazzer\"", "com.google.json.JsonSanitizer.sanitize(\"jazzer\")", + JsonSanitizer.class.getMethod("sanitize", String.class), + Arrays.asList((byte) 1, 12, "jazzer")); + + FuzzedDataProvider data = + CannedFuzzedDataProvider.create(Arrays.asList((byte) 1, // do not return null + 8, // remainingBytes + "buzz")); + assertEquals("fizzbuzz", Meta.autofuzz(data, "fizz" ::concat)); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/SettersTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/SettersTest.java new file mode 100644 index 00000000..7c869531 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/SettersTest.java @@ -0,0 +1,43 @@ +// 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.autofuzz; + +import static com.code_intelligence.jazzer.autofuzz.TestHelpers.consumeTestCase; + +import com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters; +import java.util.Arrays; +import org.junit.Test; + +public class SettersTest { + @Test + public void testEmptyConstructorWithSetters() { + EmployeeWithSetters employee = new EmployeeWithSetters(); + employee.setFirstName("foo"); + employee.setAge(26); + + consumeTestCase(employee, + "((java.util.function.Supplier<com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters>) (() -> {com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters autofuzzVariable0 = new com.code_intelligence.jazzer.autofuzz.testdata.EmployeeWithSetters(); autofuzzVariable0.setFirstName(\"foo\"); autofuzzVariable0.setAge(26); return autofuzzVariable0;})).get()", + Arrays.asList((byte) 1, // do not return null for EmployeeWithSetters + 0, // pick first constructor + 2, // pick two setters + 1, // pick second setter + 0, // pick first setter + (byte) 1, // do not return null for String + 6, // remaining bytes + "foo", // setFirstName + 26 // setAge + )); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java new file mode 100644 index 00000000..52f19a74 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.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.autofuzz; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.code_intelligence.jazzer.api.CannedFuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.List; + +class TestHelpers { + static void assertGeneralEquals(Object expected, Object actual) { + Class<?> type = expected != null ? expected.getClass() : Object.class; + if (type.isArray()) { + if (type.getComponentType() == boolean.class) { + assertArrayEquals((boolean[]) expected, (boolean[]) actual); + } else if (type.getComponentType() == char.class) { + assertArrayEquals((char[]) expected, (char[]) actual); + } else if (type.getComponentType() == short.class) { + assertArrayEquals((short[]) expected, (short[]) actual); + } else if (type.getComponentType() == long.class) { + assertArrayEquals((long[]) expected, (long[]) actual); + } else { + assertArrayEquals((Object[]) expected, (Object[]) actual); + } + } else if (type == ByteArrayInputStream.class) { + ByteArrayInputStream expectedStream = (ByteArrayInputStream) expected; + ByteArrayInputStream actualStream = (ByteArrayInputStream) actual; + assertArrayEquals(readAllBytes(expectedStream), readAllBytes(actualStream)); + } else { + assertEquals(expected, actual); + } + } + + static void consumeTestCase( + Object expectedResult, String expectedResultString, List<Object> cannedData) { + Class<?> type = expectedResult != null ? expectedResult.getClass() : Object.class; + consumeTestCase(type, expectedResult, expectedResultString, cannedData); + } + + static void consumeTestCase( + Class<?> type, Object expectedResult, String expectedResultString, List<Object> cannedData) { + assertTrue(expectedResult == null || type.isAssignableFrom(expectedResult.getClass())); + AutofuzzCodegenVisitor visitor = new AutofuzzCodegenVisitor(); + FuzzedDataProvider data = CannedFuzzedDataProvider.create(cannedData); + assertGeneralEquals(expectedResult, Meta.consume(data, type, visitor)); + assertEquals(expectedResultString, visitor.generate()); + } + + static void autofuzzTestCase(Object expectedResult, String expectedResultString, Executable func, + List<Object> cannedData) { + AutofuzzCodegenVisitor visitor = new AutofuzzCodegenVisitor(); + FuzzedDataProvider data = CannedFuzzedDataProvider.create(cannedData); + if (func instanceof Method) { + assertGeneralEquals(expectedResult, Meta.autofuzz(data, (Method) func, visitor)); + } else { + assertGeneralEquals(expectedResult, Meta.autofuzz(data, (Constructor<?>) func, visitor)); + } + assertEquals(expectedResultString, visitor.generate()); + } + + private static byte[] readAllBytes(ByteArrayInputStream in) { + byte[] result = new byte[in.available()]; + in.read(result, 0, in.available()); + return result; + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/BUILD.bazel new file mode 100644 index 00000000..c2c68803 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/BUILD.bazel @@ -0,0 +1,5 @@ +java_library( + name = "test_data", + srcs = glob(["*.java"]), + visibility = ["//visibility:public"], +) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/EmployeeWithSetters.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/EmployeeWithSetters.java new file mode 100644 index 00000000..2c76a61f --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/testdata/EmployeeWithSetters.java @@ -0,0 +1,56 @@ +// 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.autofuzz.testdata; + +import java.util.Objects; + +public class EmployeeWithSetters { + private String firstName; + private String lastName; + private String jobTitle; + private int age; + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + EmployeeWithSetters hero = (EmployeeWithSetters) o; + return age == hero.age && Objects.equals(firstName, hero.firstName) + && Objects.equals(lastName, hero.lastName) && Objects.equals(jobTitle, hero.jobTitle); + } + + @Override + public int hashCode() { + return Objects.hash(firstName, lastName, jobTitle, age); + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setJobTitle(String jobTitle) { + this.jobTitle = jobTitle; + } + + public void setAge(int age) { + this.age = age; + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooks.java new file mode 100644 index 00000000..f8d6782c --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt new file mode 100644 index 00000000..53efd200 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt @@ -0,0 +1,63 @@ +// 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 java.io.File + +private fun applyAfterHooks(bytecode: ByteArray): ByteArray { + return HookInstrumentor(loadHooks(AfterHooks::class.java), false).instrument(bytecode) +} + +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).instrument(originalBytecode) + val patchedClass = bytecodeToClass(AfterHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as AfterHooksTargetContract +} + +private fun getPatchedAfterHooksTargetInstance(): AfterHooksTargetContract { + val originalBytecode = classToBytecode(AfterHooksTarget::class.java) + val patchedBytecode = applyAfterHooks(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 AfterHookTest { + + @Test + fun testAfterHooksOriginal() { + assertSelfCheck(getOriginalAfterHooksTargetInstance(), false) + } + + @Test + fun testAfterHooksNoHooks() { + assertSelfCheck(getNoHooksAfterHooksTargetInstance(), false) + } + + @Test + fun testAfterHooksPatched() { + assertSelfCheck(getPatchedAfterHooksTargetInstance(), true) + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTarget.java new file mode 100644 index 00000000..a47b03a5 --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksTargetContract.java new file mode 100644 index 00000000..cb12b148 --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel new file mode 100644 index 00000000..472d2b98 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -0,0 +1,147 @@ +load("//bazel:kotlin.bzl", "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", + ], +) + +wrapped_kt_jvm_test( + name = "trace_data_flow_instrumentation_test", + size = "small", + srcs = [ + "MockTraceDataFlowCallbacks.java", + "TraceDataFlowInstrumentationTarget.java", + "TraceDataFlowInstrumentationTest.kt", + ], + associates = [ + "//agent/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 = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.CoverageInstrumentationTest", + deps = [ + ":patch_test_utils", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) + +wrapped_kt_jvm_test( + name = "descriptor_utils_test", + size = "small", + srcs = [ + "DescriptorUtilsTest.kt", + ], + associates = [ + "//agent/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 = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.HookValidationTest", + deps = [ + "//agent/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 = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.AfterHookTest", + deps = [ + ":patch_test_utils", + "//agent/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 = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.BeforeHookTest", + deps = [ + ":patch_test_utils", + "//agent/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", + "ReplaceHooksPatchTest.kt", + "ReplaceHooksTarget.java", + "ReplaceHooksTargetContract.java", + ], + associates = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor", + ], + test_class = "com.code_intelligence.jazzer.instrumentor.ReplaceHookTest", + deps = [ + ":patch_test_utils", + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:junit_junit", + ], +) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooks.java new file mode 100644 index 00000000..31577dad --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt new file mode 100644 index 00000000..31e9733c --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt @@ -0,0 +1,63 @@ +// 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 java.io.File + +private fun applyBeforeHooks(bytecode: ByteArray): ByteArray { + return HookInstrumentor(loadHooks(BeforeHooks::class.java), false).instrument(bytecode) +} + +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).instrument(originalBytecode) + val patchedClass = bytecodeToClass(BeforeHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as BeforeHooksTargetContract +} + +private fun getPatchedBeforeHooksTargetInstance(): BeforeHooksTargetContract { + val originalBytecode = classToBytecode(BeforeHooksTarget::class.java) + val patchedBytecode = applyBeforeHooks(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 BeforeHookTest { + + @Test + fun testBeforeHooksOriginal() { + assertSelfCheck(getOriginalBeforeHooksTargetInstance(), false) + } + + @Test + fun testBeforeHooksNoHooks() { + assertSelfCheck(getNoHooksBeforeHooksTargetInstance(), false) + } + + @Test + fun testBeforeHooksPatched() { + assertSelfCheck(getPatchedBeforeHooksTargetInstance(), true) + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTarget.java new file mode 100644 index 00000000..869e04bf --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksTargetContract.java new file mode 100644 index 00000000..61f79dcc --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationSpecialCasesTarget.java new file mode 100644 index 00000000..cb811803 --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java new file mode 100644 index 00000000..7502481d --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt new file mode 100644 index 00000000..15c88f4c --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt @@ -0,0 +1,141 @@ +// 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 java.io.File +import kotlin.test.assertEquals + +private fun applyInstrumentation(bytecode: ByteArray): ByteArray { + return EdgeCoverageInstrumentor(0, MockCoverageMap::class.java).instrument(bytecode) +} + +private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract { + return CoverageInstrumentationTarget() +} + +private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract { + val originalBytecode = classToBytecode(CoverageInstrumentationTarget::class.java) + val patchedBytecode = applyInstrumentation(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()) +} + +class CoverageInstrumentationTest { + + private val constructorReturn = 0 + private val ifFirstBranch = 1 + @Suppress("unused") + private val ifSecondBranch = 2 + private val ifEnd = 3 + private val outerForCondition = 4 + private val innerForBodyIfFirstRun = 6 + private val innerForBodyIfSecondRun = 5 + private val innerForIncrementCounter = 7 + private val outerForIncrementCounter = 8 + private val afterFooInvocation = 9 + private val beforeReturn = 10 + private val fooAfterBarInvocation = 11 + private val fooBeforeReturn = 12 + private val barAfterMapPutInvocation = 13 + private val barBeforeReturn = 14 + @Suppress("unused") + private val bazReturn = 15 + + @Test + fun testOriginal() { + assertSelfCheck(getOriginalInstrumentationTargetInstance()) + } + + @Test + fun testInstrumented() { + MockCoverageMap.clear() + assertSelfCheck(getInstrumentedInstrumentationTargetInstance()) + + val innerForFirstRunControlFlow = mutableListOf<Int>().apply { + repeat(5) { + addAll(listOf(innerForBodyIfFirstRun, innerForIncrementCounter)) + } + }.toList() + val innerForSecondRunControlFlow = mutableListOf<Int>().apply { + repeat(5) { + addAll(listOf(innerForBodyIfSecondRun, innerForIncrementCounter)) + } + }.toList() + val outerForControlFlow = + listOf(outerForCondition) + + innerForFirstRunControlFlow + + listOf(outerForIncrementCounter, outerForCondition) + + innerForSecondRunControlFlow + + listOf(outerForIncrementCounter) + + assertControlFlow( + listOf(constructorReturn, ifFirstBranch, ifEnd) + + outerForControlFlow + + listOf( + barAfterMapPutInvocation, barBeforeReturn, + fooAfterBarInvocation, fooBeforeReturn, + afterFooInvocation, beforeReturn + ) + ) + } + + @OptIn(ExperimentalUnsignedTypes::class) + @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 = ifFirstBranch + + var lastCounter = 0.toUByte() + for (i in 1..600) { + assertSelfCheck(target) + assertEquals(1, MockCoverageMap.mem[takenOnceEdge]) + // Verify that the counter increments, but is never zero. + val expectedCounter = (lastCounter + 1U).toUByte().takeUnless { it == 0.toUByte() } + ?: (lastCounter + 2U).toUByte() + lastCounter = expectedCounter + val actualCounter = MockCoverageMap.mem[takenOnEveryRunEdge].toUByte() + assertEquals(expectedCounter, actualCounter, "After $i runs:") + } + } + + @Test + fun testSpecialCases() { + val originalBytecode = classToBytecode(CoverageInstrumentationSpecialCasesTarget::class.java) + val patchedBytecode = applyInstrumentation(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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt new file mode 100644 index 00000000..e7e1feba --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt @@ -0,0 +1,73 @@ +// 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.utils.descriptor +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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/DynamicTestContract.java new file mode 100644 index 00000000..163b226a --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt new file mode 100644 index 00000000..7e7c31c9 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt @@ -0,0 +1,38 @@ +// 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() { + assertEquals(6, loadHooks(ValidHookMocks::class.java).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") { + Hook.verifyAndGetHook(method, method.declaredAnnotations.first() as MethodHook) + } + } + } + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java new file mode 100644 index 00000000..2723ad6e --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.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 com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +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.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) {} +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java new file mode 100644 index 00000000..787ea493 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.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 java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +public class MockCoverageMap { + public static final int SIZE = 65536; + public static final ByteBuffer mem = ByteBuffer.allocate(SIZE); + public static int prev_location = 0; // is used in byte code directly + + 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) != mem.get(i)) { + updated_pos = i; + } + } + locations.add(updated_pos); + System.arraycopy(mem.array(), 0, previous_mem.array(), 0, SIZE); + } + + public static void clear() { + Arrays.fill(mem.array(), (byte) 0); + Arrays.fill(previous_mem.array(), (byte) 0); + locations.clear(); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java new file mode 100644 index 00000000..ad659da0 --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt new file mode 100644 index 00000000..f286d03f --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt @@ -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 + +fun classToBytecode(targetClass: Class<*>): ByteArray { + return ClassLoader + .getSystemClassLoader() + .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!! + .use { + it.readBytes() + } +} + +fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> { + return BytecodeClassLoader(name, bytecode).loadClass(name) +} + +/** + * 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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java new file mode 100644 index 00000000..a71e1180 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java @@ -0,0 +1,109 @@ +// 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 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; + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt new file mode 100644 index 00000000..76fb53e5 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt @@ -0,0 +1,63 @@ +// 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 java.io.File + +private fun applyReplaceHooks(bytecode: ByteArray): ByteArray { + return HookInstrumentor(loadHooks(ReplaceHooks::class.java), false).instrument(bytecode) +} + +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).instrument(originalBytecode) + val patchedClass = bytecodeToClass(ReplaceHooksTarget::class.java.name, patchedBytecode) + return patchedClass.getDeclaredConstructor().newInstance() as ReplaceHooksTargetContract +} + +private fun getPatchedReplaceHooksTargetInstance(): ReplaceHooksTargetContract { + val originalBytecode = classToBytecode(ReplaceHooksTarget::class.java) + val patchedBytecode = applyReplaceHooks(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 ReplaceHookTest { + + @Test + fun testReplaceHooksOriginal() { + assertSelfCheck(getOriginalReplaceHooksTargetInstance(), false) + } + + @Test + fun testReplaceHooksNoHooks() { + assertSelfCheck(getNoHooksReplaceHooksTargetInstance(), false) + } + + @Test + fun testReplaceHooksPatched() { + assertSelfCheck(getPatchedReplaceHooksTargetInstance(), true) + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java new file mode 100644 index 00000000..7a4b89f8 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java @@ -0,0 +1,120 @@ +// 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.AbstractList; +import java.util.ArrayList; +import java.util.HashMap; +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(); + } + + AbstractList<Boolean> boolList = new ArrayList<>(); + boolList.add(false); + results.put("arrayListGet", boolList.get(0)); + + 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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTargetContract.java new file mode 100644 index 00000000..e3dff93e --- /dev/null +++ b/agent/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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java new file mode 100644 index 00000000..48f16e60 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java @@ -0,0 +1,152 @@ +// 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; + + @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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt new file mode 100644 index 00000000..c6fd218f --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt @@ -0,0 +1,145 @@ +// 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 java.io.File + +private fun applyInstrumentation(bytecode: ByteArray): ByteArray { + return TraceDataFlowInstrumentor( + setOf( + InstrumentationType.CMP, + InstrumentationType.DIV, + InstrumentationType.GEP + ), + MockTraceDataFlowCallbacks::class.java + ).instrument(bytecode) +} + +private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract { + return TraceDataFlowInstrumentationTarget() +} + +private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract { + val originalBytecode = classToBytecode(TraceDataFlowInstrumentationTarget::class.java) + val patchedBytecode = applyInstrumentation(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/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java new file mode 100644 index 00000000..06bed141 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java @@ -0,0 +1,49 @@ +// 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.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + public static void validAfterHook2(MethodHandle method, String thisObject, Object[] arguments, + int hookId, boolean returnValue) {} + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.String", + targetMethod = "equals", targetMethodDescriptor = "(Ljava/lang/Object;)Z") + public static Boolean + 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; + } +} |