Skip to content

Commit 2ccf382

Browse files
committed
chore: add test to validate hook method descriptors
1 parent 4cefe2a commit 2ccf382

5 files changed

Lines changed: 234 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: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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+
40+
final Set<String> SKIPPED_CLASSES_CURRENT_JDK =
41+
Collections.unmodifiableSet(
42+
Stream.of(
43+
// Available for JDK >= 9
44+
"java.util.regex.Pattern$Single",
45+
"java.util.regex.Pattern$SingleI",
46+
"java.util.regex.Pattern$SingleS",
47+
"java.util.regex.Pattern$SingleU")
48+
.collect(Collectors.toSet()));
49+
50+
final Set<String> SKIPPED_METHODS_CURRENT_JDK =
51+
Collections.unmodifiableSet(
52+
Stream.of(
53+
// Available for JDK >= 9
54+
"java.util.regex.Pattern.caseInsensitiveRangeFor",
55+
"java.util.regex.Pattern.rangeFor",
56+
"java.util.regex.Pattern.union",
57+
"sun.misc.Unsafe.getByte",
58+
"sun.misc.Unsafe.getChar",
59+
"sun.misc.Unsafe.getDouble",
60+
"sun.misc.Unsafe.getFloat",
61+
"sun.misc.Unsafe.getInt",
62+
"sun.misc.Unsafe.getLong",
63+
"sun.misc.Unsafe.getShort",
64+
"sun.misc.Unsafe.putByte",
65+
"sun.misc.Unsafe.putChar",
66+
"sun.misc.Unsafe.putDouble",
67+
"sun.misc.Unsafe.putFloat",
68+
"sun.misc.Unsafe.putInt",
69+
"sun.misc.Unsafe.putLong",
70+
"sun.misc.Unsafe.putShort")
71+
.collect(Collectors.toSet()));
72+
73+
final Set<String> SKIPPED_CLASSES_JDK_8 =
74+
Collections.unmodifiableSet(
75+
Stream.of(
76+
"jakarta.el.ExpressionFactory",
77+
"java.util.regex.Pattern$CharPredicate",
78+
"javax.xml.xpath.XPath")
79+
.collect(Collectors.toSet()));
80+
81+
final Set<String> SKIPPED_METHODS_JDK_8 =
82+
Collections.unmodifiableSet(
83+
Stream.of(
84+
"java.lang.Class.forName",
85+
"java.lang.ClassLoader.loadClass",
86+
"java.nio.file.Files.mismatch",
87+
"java.nio.file.Files.readString",
88+
"java.nio.file.Files.writeString",
89+
"java.util.regex.Pattern.CIRange",
90+
"java.util.regex.Pattern.CIRangeU",
91+
"java.util.regex.Pattern.Range",
92+
"java.util.regex.Pattern.Single",
93+
"java.util.regex.Pattern.SingleI",
94+
"java.util.regex.Pattern.SingleS",
95+
"java.util.regex.Pattern.SingleU")
96+
.collect(Collectors.toSet()));
97+
98+
final boolean isJDK8 = System.getProperty("java.version").startsWith("1.8");
99+
final Set<String> SKIPPED_CLASSES = isJDK8 ? SKIPPED_CLASSES_JDK_8 : SKIPPED_CLASSES_CURRENT_JDK;
100+
final Set<String> SKIPPED_METHODS = isJDK8 ? SKIPPED_METHODS_JDK_8 : SKIPPED_METHODS_CURRENT_JDK;
101+
102+
@ParameterizedTest
103+
@MethodSource("getMethodHooks")
104+
public void methodHookResolves(MethodHook hook) {
105+
String targetClassName = hook.targetClassName();
106+
assertNotNull(targetClassName, "Hook has no target class");
107+
if (SKIPPED_CLASSES.contains(targetClassName)) {
108+
return;
109+
}
110+
ClassLoader loader = HookBindingSanityTest.class.getClassLoader();
111+
Class<?> targetClass =
112+
assertDoesNotThrow(
113+
() -> Class.forName(targetClassName, false, loader),
114+
() -> "class to hook not found: " + targetClassName);
115+
String methodName = hook.targetMethod();
116+
String methodDesc = hook.targetMethodDescriptor();
117+
118+
if (SKIPPED_METHODS.contains(String.format("%s.%s", targetClassName, methodName))) {
119+
return;
120+
}
121+
122+
if ("<init>".equals(methodName)) {
123+
if (methodDesc.isEmpty()) {
124+
// Any constructor is acceptable.
125+
assertNotEquals(
126+
0,
127+
targetClass.getDeclaredConstructors().length,
128+
String.format("no constructor for class %s found", targetClassName));
129+
} else {
130+
// Match specific constructor by descriptor
131+
MethodType mt = MethodType.fromMethodDescriptorString(methodDesc, loader);
132+
Class<?>[] descriptorParams = mt.parameterArray();
133+
assertTrue(
134+
Arrays.stream(targetClass.getDeclaredConstructors())
135+
.anyMatch(c -> Arrays.equals(c.getParameterTypes(), descriptorParams)),
136+
String.format("no matching constructor for class %s found", targetClassName));
137+
}
138+
} else {
139+
if (methodDesc.isEmpty()) {
140+
// Require at least one declared method with that name
141+
assertTrue(
142+
Arrays.stream(targetClass.getDeclaredMethods())
143+
.anyMatch(md -> md.getName().equals(methodName)),
144+
String.format("method name %s not found in class %s", methodName, targetClassName));
145+
} else {
146+
MethodType mt = MethodType.fromMethodDescriptorString(methodDesc, loader);
147+
Class<?> descriptorReturnType = mt.returnType();
148+
Class<?>[] descriptorParams = mt.parameterArray();
149+
assertTrue(
150+
Arrays.stream(targetClass.getDeclaredMethods())
151+
.anyMatch(
152+
md ->
153+
md.getName().equals(methodName)
154+
&& md.getReturnType().equals(descriptorReturnType)
155+
&& Arrays.equals(md.getParameterTypes(), descriptorParams)),
156+
String.format(
157+
"method %s with descriptor %s not found in class %s",
158+
methodName, methodDesc, targetClassName));
159+
}
160+
}
161+
}
162+
163+
static Class<?> getHookClass(String className) {
164+
try {
165+
return Class.forName(className, false, HookBindingSanityTest.class.getClassLoader());
166+
} catch (ClassNotFoundException e) {
167+
throw new RuntimeException("Could not find hook class " + className, e);
168+
}
169+
}
170+
171+
static MethodHook[] getMethodHooks() throws ClassNotFoundException, NoSuchMethodException {
172+
return Constants.SANITIZER_HOOK_NAMES.stream()
173+
.map(HookBindingSanityTest::getHookClass)
174+
.flatMap(clazz -> Stream.of(clazz.getMethods()))
175+
.flatMap(
176+
m ->
177+
Stream.concat(
178+
Stream.of(m.getAnnotation(MethodHook.class)),
179+
Optional.ofNullable(m.getAnnotation(MethodHooks.class))
180+
.map(MethodHooks::value)
181+
.map(Stream::of)
182+
.orElseGet(Stream::empty)))
183+
.filter(Objects::nonNull)
184+
.toArray(MethodHook[]::new);
185+
}
186+
}

0 commit comments

Comments
 (0)