Skip to content

Commit d88ad22

Browse files
committed
chore: add test to validate hook method descriptors
1 parent b5bf267 commit d88ad22

File tree

5 files changed

+291
-3
lines changed

5 files changed

+291
-3
lines changed

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.5",
108108
"com.google.truth.extensions:truth-proto-extension:1.4.5",
109109
"com.google.truth:truth:1.4.5",
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.27.6",
112114
"org.jacoco:org.jacoco.core:0.8.14",

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": 490805884,
4-
"__RESOLVED_ARTIFACTS_HASH": 1836346034,
3+
"__INPUT_ARTIFACTS_HASH": -1001008825,
4+
"__RESOLVED_ARTIFACTS_HASH": 1591993774,
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.26.1": "com.google.errorprone:error_prone_annotations:2.41.0",
@@ -202,6 +202,12 @@
202202
},
203203
"version": "1.12.3"
204204
},
205+
"jakarta.el:jakarta.el-api": {
206+
"shasums": {
207+
"jar": "7e84b5bed49de32b79cc5e85d90b6f5adb1a953ac67283adbb41c1e297f9c605"
208+
},
209+
"version": "6.0.1"
210+
},
205211
"jakarta.servlet:jakarta.servlet-api": {
206212
"shasums": {
207213
"jar": "c034eb1afb158987dbb53a5fea0cadf611c8dae8daadd59c44d9d5ab70129cef"
@@ -220,6 +226,12 @@
220226
},
221227
"version": "3.0.1-b06"
222228
},
229+
"javax.persistence:javax.persistence-api": {
230+
"shasums": {
231+
"jar": "5578b71b37999a5eaed3fea0d14aa61c60c6ec6328256f2b63472f336318baf4"
232+
},
233+
"version": "2.2"
234+
},
223235
"javax.validation:validation-api": {
224236
"shasums": {
225237
"jar": "9873b46df1833c9ee8f5bc1ff6853375115dadd8897bcb5a0dffb5848835ee6c"
@@ -1295,6 +1307,9 @@
12951307
"io.micrometer.observation.docs",
12961308
"io.micrometer.observation.transport"
12971309
],
1310+
"jakarta.el:jakarta.el-api": [
1311+
"jakarta.el"
1312+
],
12981313
"jakarta.servlet:jakarta.servlet-api": [
12991314
"jakarta.servlet",
13001315
"jakarta.servlet.annotation",
@@ -1307,6 +1322,12 @@
13071322
"javax.el:javax.el-api": [
13081323
"javax.el"
13091324
],
1325+
"javax.persistence:javax.persistence-api": [
1326+
"javax.persistence",
1327+
"javax.persistence.criteria",
1328+
"javax.persistence.metamodel",
1329+
"javax.persistence.spi"
1330+
],
13101331
"javax.validation:validation-api": [
13111332
"javax.validation",
13121333
"javax.validation.bootstrap",
@@ -2628,9 +2649,11 @@
26282649
"io.github.classgraph:classgraph",
26292650
"io.micrometer:micrometer-commons",
26302651
"io.micrometer:micrometer-observation",
2652+
"jakarta.el:jakarta.el-api",
26312653
"jakarta.servlet:jakarta.servlet-api",
26322654
"javax.activation:javax.activation-api",
26332655
"javax.el:javax.el-api",
2656+
"javax.persistence:javax.persistence-api",
26342657
"javax.validation:validation-api",
26352658
"javax.xml.bind:jaxb-api",
26362659
"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: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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.assertDoesNotThrow;
19+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
import com.code_intelligence.jazzer.api.MethodHook;
24+
import com.code_intelligence.jazzer.api.MethodHooks;
25+
import java.lang.invoke.MethodType;
26+
import java.util.Arrays;
27+
import java.util.Collections;
28+
import java.util.Objects;
29+
import java.util.Optional;
30+
import java.util.Set;
31+
import java.util.stream.Collectors;
32+
import java.util.stream.Stream;
33+
import org.junit.jupiter.params.ParameterizedTest;
34+
import org.junit.jupiter.params.provider.MethodSource;
35+
36+
/**
37+
* Verifies that for every declared @MethodHook in built-in sanitizers, a corresponding target
38+
* method (or constructor) with the configured descriptor exists. This guards against typos and
39+
* wrong descriptors.
40+
*/
41+
public class HookBindingSanityTest {
42+
static class MethodRef {
43+
String className;
44+
String methodName;
45+
String descriptor;
46+
47+
MethodRef(String className) {
48+
this.className = className;
49+
}
50+
51+
MethodRef(String className, String methodName, String descriptor) {
52+
this.className = className;
53+
this.methodName = methodName;
54+
this.descriptor = descriptor;
55+
}
56+
57+
@Override
58+
public int hashCode() {
59+
return Objects.hash(className, methodName, descriptor);
60+
}
61+
62+
@Override
63+
public boolean equals(Object o) {
64+
if (!(o instanceof MethodRef)) return false;
65+
MethodRef other = (MethodRef) o;
66+
return Objects.equals(this.className, other.className)
67+
&& Objects.equals(this.methodName, other.methodName)
68+
&& Objects.equals(this.descriptor, other.descriptor);
69+
}
70+
}
71+
72+
// Classes or methods that are not available in JDK version > 8.
73+
final Set<MethodRef> SKIPPED_CURRENT_JDK =
74+
Collections.unmodifiableSet(
75+
Stream.of(
76+
new MethodRef("java.util.regex.Pattern$Single"),
77+
new MethodRef("java.util.regex.Pattern$SingleI"),
78+
new MethodRef("java.util.regex.Pattern$SingleS"),
79+
new MethodRef("java.util.regex.Pattern$SingleU"),
80+
new MethodRef(
81+
"java.util.regex.Pattern",
82+
"caseInsensitiveRangeFor",
83+
"(II)Ljava/util/regex/Pattern$CharProperty;"),
84+
new MethodRef(
85+
"java.util.regex.Pattern",
86+
"rangeFor",
87+
"(II)Ljava/util/regex/Pattern$CharProperty;"),
88+
new MethodRef(
89+
"java.util.regex.Pattern",
90+
"union",
91+
"(Ljava/util/regex/Pattern$CharProperty;Ljava/util/regex/Pattern$CharProperty;)Ljava/util/regex/Pattern$CharProperty;"),
92+
new MethodRef("sun.misc.Unsafe", "getByte", "(Ljava/lang/Object;I)B"),
93+
new MethodRef("sun.misc.Unsafe", "getChar", "(Ljava/lang/Object;I)C"),
94+
new MethodRef("sun.misc.Unsafe", "getDouble", "(Ljava/lang/Object;I)D"),
95+
new MethodRef("sun.misc.Unsafe", "getFloat", "(Ljava/lang/Object;I)F"),
96+
new MethodRef("sun.misc.Unsafe", "getInt", "(Ljava/lang/Object;I)I"),
97+
new MethodRef("sun.misc.Unsafe", "getLong", "(Ljava/lang/Object;I)J"),
98+
new MethodRef("sun.misc.Unsafe", "getShort", "(Ljava/lang/Object;I)S"),
99+
new MethodRef("sun.misc.Unsafe", "putByte", "(Ljava/lang/Object;IB)V"),
100+
new MethodRef("sun.misc.Unsafe", "putChar", "(Ljava/lang/Object;IC)V"),
101+
new MethodRef("sun.misc.Unsafe", "putDouble", "(Ljava/lang/Object;ID)V"),
102+
new MethodRef("sun.misc.Unsafe", "putFloat", "(Ljava/lang/Object;IF)V"),
103+
new MethodRef("sun.misc.Unsafe", "putInt", "(Ljava/lang/Object;II)V"),
104+
new MethodRef("sun.misc.Unsafe", "putLong", "(Ljava/lang/Object;IJ)V"),
105+
new MethodRef("sun.misc.Unsafe", "putShort", "(Ljava/lang/Object;IS)V"))
106+
.collect(Collectors.toSet()));
107+
108+
// Classes or methods that are not verified in JDK version 8.
109+
final Set<MethodRef> SKIPPED_JDK_8 =
110+
Collections.unmodifiableSet(
111+
Stream.of(
112+
new MethodRef("jakarta.el.ExpressionFactory"), // -> UnsupportedClassVersionError
113+
new MethodRef("java.util.regex.Pattern$CharPredicate"),
114+
new MethodRef("javax.xml.xpath.XPath", "evaluateExpression", null),
115+
new MethodRef(
116+
"java.lang.Class",
117+
"forName",
118+
"(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"),
119+
new MethodRef(
120+
"java.lang.ClassLoader",
121+
"loadClass",
122+
"(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"),
123+
new MethodRef("java.nio.file.Files", "mismatch", null),
124+
new MethodRef("java.nio.file.Files", "readString", null),
125+
new MethodRef("java.nio.file.Files", "writeString", null),
126+
new MethodRef(
127+
"java.util.regex.Pattern",
128+
"CIRange",
129+
"(II)Ljava/util/regex/Pattern$CharPredicate;"),
130+
new MethodRef(
131+
"java.util.regex.Pattern",
132+
"CIRangeU",
133+
"(II)Ljava/util/regex/Pattern$CharPredicate;"),
134+
new MethodRef(
135+
"java.util.regex.Pattern",
136+
"Range",
137+
"(II)Ljava/util/regex/Pattern$CharPredicate;"),
138+
new MethodRef(
139+
"java.util.regex.Pattern",
140+
"Single",
141+
"(I)Ljava/util/regex/Pattern$BmpCharPredicate;"),
142+
new MethodRef(
143+
"java.util.regex.Pattern",
144+
"SingleI",
145+
"(II)Ljava/util/regex/Pattern$BmpCharPredicate;"),
146+
new MethodRef(
147+
"java.util.regex.Pattern",
148+
"SingleS",
149+
"(I)Ljava/util/regex/Pattern$CharPredicate;"),
150+
new MethodRef(
151+
"java.util.regex.Pattern",
152+
"SingleU",
153+
"(I)Ljava/util/regex/Pattern$CharPredicate;"))
154+
.collect(Collectors.toSet()));
155+
156+
final boolean isJDK8 = System.getProperty("java.version").startsWith("1.8");
157+
final Set<MethodRef> SKIPPED = isJDK8 ? SKIPPED_JDK_8 : SKIPPED_CURRENT_JDK;
158+
159+
@ParameterizedTest
160+
@MethodSource("getMethodHooks")
161+
public void methodHookResolves(MethodHook hook) {
162+
String targetClassName = hook.targetClassName();
163+
assertNotNull(targetClassName, "Hook has no target class");
164+
if (SKIPPED.contains(new MethodRef(targetClassName))) {
165+
return;
166+
}
167+
ClassLoader loader = HookBindingSanityTest.class.getClassLoader();
168+
Class<?> targetClass =
169+
assertDoesNotThrow(
170+
() -> Class.forName(targetClassName, false, loader),
171+
() -> "class to hook not found: " + targetClassName);
172+
String methodName = hook.targetMethod();
173+
String methodDesc = hook.targetMethodDescriptor();
174+
methodDesc = (methodDesc != null && !methodDesc.isEmpty()) ? methodDesc : null;
175+
if (SKIPPED.contains(new MethodRef(targetClassName, methodName, methodDesc))) {
176+
return;
177+
}
178+
179+
if ("<init>".equals(methodName)) {
180+
if (methodDesc == null) {
181+
// Any constructor is acceptable.
182+
assertNotEquals(
183+
0,
184+
targetClass.getDeclaredConstructors().length,
185+
String.format("no constructor for class %s found", targetClassName));
186+
} else {
187+
// Match specific constructor by descriptor
188+
MethodType mt = MethodType.fromMethodDescriptorString(methodDesc, loader);
189+
Class<?>[] descriptorParams = mt.parameterArray();
190+
assertTrue(
191+
Arrays.stream(targetClass.getDeclaredConstructors())
192+
.anyMatch(c -> Arrays.equals(c.getParameterTypes(), descriptorParams)),
193+
String.format("no matching constructor for class %s found", targetClassName));
194+
}
195+
} else {
196+
if (methodDesc == null) {
197+
// Require at least one declared method with that name
198+
assertTrue(
199+
Arrays.stream(targetClass.getDeclaredMethods())
200+
.anyMatch(md -> md.getName().equals(methodName)),
201+
String.format("method name %s not found in class %s", methodName, targetClassName));
202+
} else {
203+
MethodType mt = MethodType.fromMethodDescriptorString(methodDesc, loader);
204+
Class<?> descriptorReturnType = mt.returnType();
205+
Class<?>[] descriptorParams = mt.parameterArray();
206+
assertTrue(
207+
Arrays.stream(targetClass.getDeclaredMethods())
208+
.anyMatch(
209+
md ->
210+
md.getName().equals(methodName)
211+
&& md.getReturnType().equals(descriptorReturnType)
212+
&& Arrays.equals(md.getParameterTypes(), descriptorParams)),
213+
String.format(
214+
"method %s with descriptor %s not found in class %s",
215+
methodName, methodDesc, targetClassName));
216+
}
217+
}
218+
}
219+
220+
static Class<?> getHookClass(String className) {
221+
try {
222+
return Class.forName(className, false, HookBindingSanityTest.class.getClassLoader());
223+
} catch (ClassNotFoundException e) {
224+
throw new RuntimeException("Could not find hook class " + className, e);
225+
}
226+
}
227+
228+
static MethodHook[] getMethodHooks() {
229+
return Constants.SANITIZER_HOOK_NAMES.stream()
230+
.map(HookBindingSanityTest::getHookClass)
231+
.flatMap(clazz -> Stream.of(clazz.getMethods()))
232+
.flatMap(
233+
m ->
234+
Stream.concat(
235+
Stream.of(m.getAnnotation(MethodHook.class)),
236+
Optional.ofNullable(m.getAnnotation(MethodHooks.class))
237+
.map(MethodHooks::value)
238+
.map(Stream::of)
239+
.orElseGet(Stream::empty)))
240+
.filter(Objects::nonNull)
241+
.toArray(MethodHook[]::new);
242+
}
243+
}

0 commit comments

Comments
 (0)