Skip to content

Commit 8511bfa

Browse files
committed
chore: add test to validate hook method descriptors
1 parent d7fc088 commit 8511bfa

5 files changed

Lines changed: 288 additions & 3 deletions

File tree

‎MODULE.bazel‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ TEST_MAVEN_ARTIFACTS = [
107107
"com.google.truth.extensions:truth-liteproto-extension:1.4.2",
108108
"com.google.truth.extensions:truth-proto-extension:1.4.2",
109109
"com.google.truth:truth:1.4.2",
110+
"jakarta.el:jakarta.el-api:6.0.1",
111+
"javax.persistence:javax.persistence-api:2.2",
110112
"junit:junit:4.13.2",
111113
"org.assertj:assertj-core:3.25.3",
112114
"org.jacoco:org.jacoco.core:0.8.12",

‎maven_install.json‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
3-
"__INPUT_ARTIFACTS_HASH": 285652305,
4-
"__RESOLVED_ARTIFACTS_HASH": -2054117877,
3+
"__INPUT_ARTIFACTS_HASH": -1935863112,
4+
"__RESOLVED_ARTIFACTS_HASH": 1203654031,
55
"conflict_resolution": {
66
"com.google.code.gson:gson:2.8.6": "com.google.code.gson:gson:2.8.9",
77
"com.google.errorprone:error_prone_annotations:2.3.2": "com.google.errorprone:error_prone_annotations:2.26.1",
@@ -197,6 +197,12 @@
197197
},
198198
"version": "1.12.3"
199199
},
200+
"jakarta.el:jakarta.el-api": {
201+
"shasums": {
202+
"jar": "7e84b5bed49de32b79cc5e85d90b6f5adb1a953ac67283adbb41c1e297f9c605"
203+
},
204+
"version": "6.0.1"
205+
},
200206
"jakarta.servlet:jakarta.servlet-api": {
201207
"shasums": {
202208
"jar": "c034eb1afb158987dbb53a5fea0cadf611c8dae8daadd59c44d9d5ab70129cef"
@@ -215,6 +221,12 @@
215221
},
216222
"version": "3.0.1-b06"
217223
},
224+
"javax.persistence:javax.persistence-api": {
225+
"shasums": {
226+
"jar": "5578b71b37999a5eaed3fea0d14aa61c60c6ec6328256f2b63472f336318baf4"
227+
},
228+
"version": "2.2"
229+
},
218230
"javax.validation:validation-api": {
219231
"shasums": {
220232
"jar": "9873b46df1833c9ee8f5bc1ff6853375115dadd8897bcb5a0dffb5848835ee6c"
@@ -1285,6 +1297,9 @@
12851297
"io.micrometer.observation.docs",
12861298
"io.micrometer.observation.transport"
12871299
],
1300+
"jakarta.el:jakarta.el-api": [
1301+
"jakarta.el"
1302+
],
12881303
"jakarta.servlet:jakarta.servlet-api": [
12891304
"jakarta.servlet",
12901305
"jakarta.servlet.annotation",
@@ -1297,6 +1312,12 @@
12971312
"javax.el:javax.el-api": [
12981313
"javax.el"
12991314
],
1315+
"javax.persistence:javax.persistence-api": [
1316+
"javax.persistence",
1317+
"javax.persistence.criteria",
1318+
"javax.persistence.metamodel",
1319+
"javax.persistence.spi"
1320+
],
13001321
"javax.validation:validation-api": [
13011322
"javax.validation",
13021323
"javax.validation.bootstrap",
@@ -2611,9 +2632,11 @@
26112632
"io.github.classgraph:classgraph",
26122633
"io.micrometer:micrometer-commons",
26132634
"io.micrometer:micrometer-observation",
2635+
"jakarta.el:jakarta.el-api",
26142636
"jakarta.servlet:jakarta.servlet-api",
26152637
"javax.activation:javax.activation-api",
26162638
"javax.el:javax.el-api",
2639+
"javax.persistence:javax.persistence-api",
26172640
"javax.validation:validation-api",
26182641
"javax.xml.bind:jaxb-api",
26192642
"junit:junit",

‎sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ kt_jvm_library(
7474
"Utils.kt",
7575
"XPathInjection.kt",
7676
],
77-
visibility = ["//sanitizers:__pkg__"],
77+
visibility = [
78+
"//sanitizers:__pkg__",
79+
"//sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers:__pkg__",
80+
],
7881
runtime_deps = [
7982
":clojure_lang_hooks",
8083
":file_path_traversal",

‎sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,20 @@ java_junit5_test(
1010
"@maven//:org_junit_jupiter_junit_jupiter_params",
1111
],
1212
)
13+
14+
java_junit5_test(
15+
name = "HookBindingSanityTest",
16+
srcs = ["HookBindingSanityTest.java"],
17+
deps = JUNIT5_DEPS + [
18+
"//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers",
19+
"//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:constants",
20+
"//src/main/java/com/code_intelligence/jazzer/api:hooks",
21+
"@clojure_jar//jar",
22+
"@maven//:jakarta_el_jakarta_el_api",
23+
"@maven//:javax_el_javax_el_api",
24+
"@maven//:javax_persistence_javax_persistence_api",
25+
"@maven//:javax_validation_validation_api",
26+
"@maven//:org_junit_jupiter_junit_jupiter_api",
27+
"@maven//:org_junit_jupiter_junit_jupiter_params",
28+
],
29+
)
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.code_intelligence.jazzer.sanitizers;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
20+
import com.code_intelligence.jazzer.api.MethodHook;
21+
import com.code_intelligence.jazzer.api.MethodHooks;
22+
import java.lang.invoke.MethodType;
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import java.util.Objects;
26+
import java.util.Optional;
27+
import java.util.Set;
28+
import java.util.stream.Collectors;
29+
import java.util.stream.Stream;
30+
import org.junit.jupiter.params.ParameterizedTest;
31+
import org.junit.jupiter.params.provider.MethodSource;
32+
33+
/**
34+
* Verifies that for every declared @MethodHook in built-in sanitizers, a corresponding target
35+
* method (or constructor) with the configured descriptor exists. This guards against typos and
36+
* wrong descriptors.
37+
*/
38+
public class HookBindingSanityTest {
39+
static class MethodRef {
40+
String className;
41+
String methodName;
42+
String descriptor;
43+
44+
MethodRef(String className) {
45+
this.className = className;
46+
}
47+
48+
MethodRef(String className, String methodName, String descriptor) {
49+
this.className = className;
50+
this.methodName = methodName;
51+
this.descriptor = descriptor;
52+
}
53+
54+
@Override
55+
public int hashCode() {
56+
return Objects.hash(className, methodName, descriptor);
57+
}
58+
59+
@Override
60+
public boolean equals(Object o) {
61+
if (!(o instanceof MethodRef)) return false;
62+
MethodRef other = (MethodRef) o;
63+
return Objects.equals(this.className, other.className)
64+
&& Objects.equals(this.methodName, other.methodName)
65+
&& Objects.equals(this.descriptor, other.descriptor);
66+
}
67+
}
68+
69+
// Classes or methods that are not available in JDK version > 8.
70+
final Set<MethodRef> SKIPPED_CURRENT_JDK =
71+
Collections.unmodifiableSet(
72+
Stream.of(
73+
new MethodRef("java.util.regex.Pattern$Single"),
74+
new MethodRef("java.util.regex.Pattern$SingleI"),
75+
new MethodRef("java.util.regex.Pattern$SingleS"),
76+
new MethodRef("java.util.regex.Pattern$SingleU"),
77+
new MethodRef(
78+
"java.util.regex.Pattern",
79+
"caseInsensitiveRangeFor",
80+
"(II)Ljava/util/regex/Pattern$CharProperty;"),
81+
new MethodRef(
82+
"java.util.regex.Pattern",
83+
"rangeFor",
84+
"(II)Ljava/util/regex/Pattern$CharProperty;"),
85+
new MethodRef(
86+
"java.util.regex.Pattern",
87+
"union",
88+
"(Ljava/util/regex/Pattern$CharProperty;Ljava/util/regex/Pattern$CharProperty;)Ljava/util/regex/Pattern$CharProperty;"),
89+
new MethodRef("sun.misc.Unsafe", "getByte", "(Ljava/lang/Object;I)B"),
90+
new MethodRef("sun.misc.Unsafe", "getChar", "(Ljava/lang/Object;I)C"),
91+
new MethodRef("sun.misc.Unsafe", "getDouble", "(Ljava/lang/Object;I)D"),
92+
new MethodRef("sun.misc.Unsafe", "getFloat", "(Ljava/lang/Object;I)F"),
93+
new MethodRef("sun.misc.Unsafe", "getInt", "(Ljava/lang/Object;I)I"),
94+
new MethodRef("sun.misc.Unsafe", "getLong", "(Ljava/lang/Object;I)J"),
95+
new MethodRef("sun.misc.Unsafe", "getShort", "(Ljava/lang/Object;I)S"),
96+
new MethodRef("sun.misc.Unsafe", "putByte", "(Ljava/lang/Object;IB)V"),
97+
new MethodRef("sun.misc.Unsafe", "putChar", "(Ljava/lang/Object;IC)V"),
98+
new MethodRef("sun.misc.Unsafe", "putDouble", "(Ljava/lang/Object;ID)V"),
99+
new MethodRef("sun.misc.Unsafe", "putFloat", "(Ljava/lang/Object;IF)V"),
100+
new MethodRef("sun.misc.Unsafe", "putInt", "(Ljava/lang/Object;II)V"),
101+
new MethodRef("sun.misc.Unsafe", "putLong", "(Ljava/lang/Object;IJ)V"),
102+
new MethodRef("sun.misc.Unsafe", "putShort", "(Ljava/lang/Object;IS)V"))
103+
.collect(Collectors.toSet()));
104+
105+
// Classes or methods that are not verified in JDK version 8.
106+
final Set<MethodRef> SKIPPED_JDK_8 =
107+
Collections.unmodifiableSet(
108+
Stream.of(
109+
new MethodRef("jakarta.el.ExpressionFactory"), // -> UnsupportedClassVersionError
110+
new MethodRef("java.util.regex.Pattern$CharPredicate"),
111+
new MethodRef("javax.xml.xpath.XPath", "evaluateExpression", null),
112+
new MethodRef(
113+
"java.lang.Class",
114+
"forName",
115+
"(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"),
116+
new MethodRef(
117+
"java.lang.ClassLoader",
118+
"loadClass",
119+
"(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"),
120+
new MethodRef("java.nio.file.Files", "mismatch", null),
121+
new MethodRef("java.nio.file.Files", "readString", null),
122+
new MethodRef("java.nio.file.Files", "writeString", null),
123+
new MethodRef(
124+
"java.util.regex.Pattern",
125+
"CIRange",
126+
"(II)Ljava/util/regex/Pattern$CharPredicate;"),
127+
new MethodRef(
128+
"java.util.regex.Pattern",
129+
"CIRangeU",
130+
"(II)Ljava/util/regex/Pattern$CharPredicate;"),
131+
new MethodRef(
132+
"java.util.regex.Pattern",
133+
"Range",
134+
"(II)Ljava/util/regex/Pattern$CharPredicate;"),
135+
new MethodRef(
136+
"java.util.regex.Pattern",
137+
"Single",
138+
"(I)Ljava/util/regex/Pattern$BmpCharPredicate;"),
139+
new MethodRef(
140+
"java.util.regex.Pattern",
141+
"SingleI",
142+
"(II)Ljava/util/regex/Pattern$BmpCharPredicate;"),
143+
new MethodRef(
144+
"java.util.regex.Pattern",
145+
"SingleS",
146+
"(I)Ljava/util/regex/Pattern$CharPredicate;"),
147+
new MethodRef(
148+
"java.util.regex.Pattern",
149+
"SingleU",
150+
"(I)Ljava/util/regex/Pattern$CharPredicate;"))
151+
.collect(Collectors.toSet()));
152+
153+
final boolean isJDK8 = System.getProperty("java.version").startsWith("1.8");
154+
final Set<MethodRef> SKIPPED = isJDK8 ? SKIPPED_JDK_8 : SKIPPED_CURRENT_JDK;
155+
156+
@ParameterizedTest
157+
@MethodSource("getMethodHooks")
158+
public void methodHookResolves(MethodHook hook) {
159+
String targetClassName = hook.targetClassName();
160+
assertNotNull(targetClassName, "Hook has no target class");
161+
if (SKIPPED.contains(new MethodRef(targetClassName))) {
162+
return;
163+
}
164+
ClassLoader loader = HookBindingSanityTest.class.getClassLoader();
165+
Class<?> targetClass =
166+
assertDoesNotThrow(
167+
() -> Class.forName(targetClassName, false, loader),
168+
() -> "class to hook not found: " + targetClassName);
169+
String methodName = hook.targetMethod();
170+
String methodDesc = hook.targetMethodDescriptor();
171+
methodDesc = (methodDesc != null && !methodDesc.isEmpty()) ? methodDesc : null;
172+
if (SKIPPED.contains(new MethodRef(targetClassName, methodName, methodDesc))) {
173+
return;
174+
}
175+
176+
if ("<init>".equals(methodName)) {
177+
if (methodDesc == null) {
178+
// Any constructor is acceptable.
179+
assertNotEquals(
180+
0,
181+
targetClass.getDeclaredConstructors().length,
182+
String.format("no constructor for class %s found", targetClassName));
183+
} else {
184+
// Match specific constructor by descriptor
185+
MethodType mt = MethodType.fromMethodDescriptorString(methodDesc, loader);
186+
Class<?>[] descriptorParams = mt.parameterArray();
187+
assertTrue(
188+
Arrays.stream(targetClass.getDeclaredConstructors())
189+
.anyMatch(c -> Arrays.equals(c.getParameterTypes(), descriptorParams)),
190+
String.format("no matching constructor for class %s found", targetClassName));
191+
}
192+
} else {
193+
if (methodDesc == null) {
194+
// Require at least one declared method with that name
195+
assertTrue(
196+
Arrays.stream(targetClass.getDeclaredMethods())
197+
.anyMatch(md -> md.getName().equals(methodName)),
198+
String.format("method name %s not found in class %s", methodName, targetClassName));
199+
} else {
200+
MethodType mt = MethodType.fromMethodDescriptorString(methodDesc, loader);
201+
Class<?> descriptorReturnType = mt.returnType();
202+
Class<?>[] descriptorParams = mt.parameterArray();
203+
assertTrue(
204+
Arrays.stream(targetClass.getDeclaredMethods())
205+
.anyMatch(
206+
md ->
207+
md.getName().equals(methodName)
208+
&& md.getReturnType().equals(descriptorReturnType)
209+
&& Arrays.equals(md.getParameterTypes(), descriptorParams)),
210+
String.format(
211+
"method %s with descriptor %s not found in class %s",
212+
methodName, methodDesc, targetClassName));
213+
}
214+
}
215+
}
216+
217+
static Class<?> getHookClass(String className) {
218+
try {
219+
return Class.forName(className, false, HookBindingSanityTest.class.getClassLoader());
220+
} catch (ClassNotFoundException e) {
221+
throw new RuntimeException("Could not find hook class " + className, e);
222+
}
223+
}
224+
225+
static MethodHook[] getMethodHooks() throws ClassNotFoundException, NoSuchMethodException {
226+
return Constants.SANITIZER_HOOK_NAMES.stream()
227+
.map(HookBindingSanityTest::getHookClass)
228+
.flatMap(clazz -> Stream.of(clazz.getMethods()))
229+
.flatMap(
230+
m ->
231+
Stream.concat(
232+
Stream.of(m.getAnnotation(MethodHook.class)),
233+
Optional.ofNullable(m.getAnnotation(MethodHooks.class))
234+
.map(MethodHooks::value)
235+
.map(Stream::of)
236+
.orElseGet(Stream::empty)))
237+
.filter(Objects::nonNull)
238+
.toArray(MethodHook[]::new);
239+
}
240+
}

0 commit comments

Comments
 (0)