Skip to content

Commit bdc1c19

Browse files
dougqhclaude
andcommitted
Fix ClassLatch's JDK-version NoSuchMethodError handling and require a method-name check
tryApplyOrNull previously only caught NoSuchMethodError, missing the BootstrapMethodError-wrapped form that some JVM versions raise for the same linkage failure, which let it escape the latch uncaught. handleAbstractMethod/latchIfNamed/isNamedIn now also take the method name the call site expects, since a receiver's own implementation can raise an AbstractMethodError naming the key class while blaming a different method it calls internally; without the check that error would incorrectly latch the class for a method it does implement. Extracts the duplicated compile() test helper into CompilingClassLoaders. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent d176106 commit bdc1c19

7 files changed

Lines changed: 321 additions & 84 deletions

File tree

‎dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/JDBCDecorator.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public class JDBCDecorator extends DatabaseClientDecorator<DBInfo> {
5454
new ClassLatch<Connection, Properties, SQLException>() {
5555
@Override
5656
protected Properties apply(Connection connection) throws SQLException {
57-
return handleAbstractMethod(connection, Connection::getClientInfo);
57+
return handleAbstractMethod(connection, "getClientInfo", Connection::getClientInfo);
5858
}
5959
};
6060

‎internal-api/src/jmh/java/datadog/trace/util/ClassLatchBenchmark.java‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,23 +154,23 @@ private static Object invokeHandle(Object target) {
154154
new ClassLatch<Object, Object, RuntimeException>() {
155155
@Override
156156
protected Object apply(Object target) {
157-
return handleAbstractMethod(target, ClassLatchBenchmark::invokeHandle);
157+
return handleAbstractMethod(target, "b", ClassLatchBenchmark::invokeHandle);
158158
}
159159
};
160160

161161
private static final ClassLatch<Object, Object, RuntimeException> WRAPPER =
162162
new ClassLatch<Object, Object, RuntimeException>() {
163163
@Override
164164
protected Object apply(Object target) {
165-
return handleAbstractMethod(target, ClassLatchBenchmark::invokeHandle);
165+
return handleAbstractMethod(target, "b", ClassLatchBenchmark::invokeHandle);
166166
}
167167
};
168168

169169
private static final ClassLatch<Object, Object, RuntimeException> PRESENT =
170170
new ClassLatch<Object, Object, RuntimeException>() {
171171
@Override
172172
protected Object apply(Object target) {
173-
return handleAbstractMethod(target, ClassLatchBenchmark::invokeHandle);
173+
return handleAbstractMethod(target, "b", ClassLatchBenchmark::invokeHandle);
174174
}
175175
};
176176

@@ -183,7 +183,7 @@ protected final Object apply(Object target) {
183183
try {
184184
return invoke(target);
185185
} catch (AbstractMethodError e) {
186-
latchIfNamed(target, e);
186+
latchIfNamed(target, "b", e);
187187
return null;
188188
} catch (UnsupportedOperationException e) {
189189
return null;

‎internal-api/src/main/java/datadog/trace/util/ClassLatch.java‎

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,26 @@ protected Class<?> keyOf(T target) {
6969
*/
7070
@Nullable
7171
public final R tryApplyOrNull(@Nullable T target) throws E {
72-
return target == null || isLatched(target) ? null : apply(target);
72+
if (target == null || isLatched(target)) {
73+
return null;
74+
}
75+
try {
76+
return apply(target);
77+
} catch (NoSuchMethodError e) {
78+
// a method reference passed to handleNoSuchMethod resolves where it is written, in apply's
79+
// own frame, so a missing target method surfaces here, outside the helper's own try, as the
80+
// invokedynamic call site's own linkage failure; HotSpot reports this directly as
81+
// NoSuchMethodError on some JVM versions
82+
latch(target);
83+
return null;
84+
} catch (BootstrapMethodError e) {
85+
// on other JVM versions the same linkage failure is wrapped instead
86+
if (e.getCause() instanceof NoSuchMethodError) {
87+
latch(target);
88+
return null;
89+
}
90+
throw e;
91+
}
7392
}
7493

7594
/**
@@ -103,12 +122,14 @@ protected final void unlatch(T target) {
103122
}
104123

105124
/**
106-
* Latches the target's key if the error's message names that class as the receiver that lacks the
107-
* method. Returns whether it latched. An error that does not name the key, for example one thrown
108-
* inside a wrapper's delegate, is left alone.
125+
* Latches the target's key if the error's message names that class as the receiver that lacks
126+
* {@code methodName}. Returns whether it latched. An error that does not name the key, for
127+
* example one thrown inside a wrapper's delegate, is left alone — and so is one that names the
128+
* key but for a different method, for example one the guarded method's own implementation calls
129+
* internally.
109130
*/
110-
protected final boolean latchIfNamed(T target, AbstractMethodError error) {
111-
if (isNamedIn(error, keyOf(target))) {
131+
protected final boolean latchIfNamed(T target, String methodName, AbstractMethodError error) {
132+
if (isNamedIn(error, keyOf(target), methodName)) {
112133
latch(target);
113134
return true;
114135
}
@@ -124,7 +145,7 @@ protected final boolean latchIfNamed(T target, AbstractMethodError error) {
124145
*
125146
* <pre>{@code
126147
* protected Properties apply(Connection c) throws SQLException {
127-
* return handleAbstractMethod(c, Connection::getClientInfo);
148+
* return handleAbstractMethod(c, "getClientInfo", Connection::getClientInfo);
128149
* }
129150
* }</pre>
130151
*
@@ -133,18 +154,23 @@ protected final boolean latchIfNamed(T target, AbstractMethodError error) {
133154
* handle; see {@link #handleNoSuchMethod} and {@link #handleNoSuchOrAbstractMethod}, and prefer
134155
* the latter when unsure which a call site can see.
135156
*
157+
* <p>{@code methodName} must be {@code call}'s own method, not merely some method of {@code T}:
158+
* the error's message can name the key class while blaming a different method that the guarded
159+
* method's implementation happens to call internally, and only a method name check tells the two
160+
* apart.
161+
*
136162
* <p>Pass a method reference or a non-capturing lambda, and keep this method small so it inlines:
137163
* that is what lets the JIT see the exact function at each call site (see {@link Strategy}).
138164
* Compose {@link #latchIfNamed} and {@link #latch} directly for anything more involved.
139165
*/
140166
@Nullable
141167
@StrategyConsumer
142-
protected final R handleAbstractMethod(T target, @Strategy ThrowingFunction<T, R, E> call)
143-
throws E {
168+
protected final R handleAbstractMethod(
169+
T target, String methodName, @Strategy ThrowingFunction<T, R, E> call) throws E {
144170
try {
145171
return call.apply(target);
146172
} catch (AbstractMethodError e) {
147-
latchIfNamed(target, e);
173+
latchIfNamed(target, methodName, e);
148174
return null;
149175
} catch (UnsupportedOperationException e) {
150176
// no class to attribute it to, and it may come from a delegate: never latched
@@ -194,16 +220,16 @@ protected final R handleNoSuchMethod(T target, @Strategy ThrowingFunction<T, R,
194220
*
195221
* <pre>{@code
196222
* protected Properties apply(Connection c) throws SQLException {
197-
* return handleNoSuchOrAbstractMethod(c, Connection::getClientInfo);
223+
* return handleNoSuchOrAbstractMethod(c, "getClientInfo", Connection::getClientInfo);
198224
* }
199225
* }</pre>
200226
*/
201227
@Nullable
202228
@StrategyConsumer
203-
protected final R handleNoSuchOrAbstractMethod(T target, @Strategy ThrowingFunction<T, R, E> call)
204-
throws E {
229+
protected final R handleNoSuchOrAbstractMethod(
230+
T target, String methodName, @Strategy ThrowingFunction<T, R, E> call) throws E {
205231
try {
206-
return handleAbstractMethod(target, call);
232+
return handleAbstractMethod(target, methodName, call);
207233
} catch (NoSuchMethodError e) {
208234
latch(target);
209235
return null;
@@ -220,9 +246,12 @@ protected final R handleNoSuchOrAbstractMethod(T target, @Strategy ThrowingFunct
220246
* </ul>
221247
*
222248
* A concrete object's class is never the abstract method's declaring class or an interface, so an
223-
* exact match can only be the receiver. An unparseable message never matches.
249+
* exact match can only be the receiver. The message also names the resolved method itself: that
250+
* method's implementation can call a different, unimplemented method on the very same receiver,
251+
* raising an error that names the key class but blames a method other than {@code methodName}, so
252+
* the method name is checked too. An unparseable message never matches.
224253
*/
225-
static boolean isNamedIn(AbstractMethodError e, Class<?> type) {
254+
static boolean isNamedIn(AbstractMethodError e, Class<?> type, String methodName) {
226255
final String message = e.getMessage();
227256
if (message == null) {
228257
return false;
@@ -232,7 +261,8 @@ static boolean isNamedIn(AbstractMethodError e, Class<?> type) {
232261
final int end = RECEIVER_PREFIX.length() + name.length();
233262
return message.startsWith(name, RECEIVER_PREFIX.length())
234263
&& message.length() > end
235-
&& message.charAt(end) == ' ';
264+
&& message.charAt(end) == ' '
265+
&& message.indexOf(methodName + "(", end) > end;
236266
}
237267
if (message.startsWith(name) && message.length() > name.length() + 1) {
238268
// JDK 8: the method name follows the class name and runs up to the descriptor, so it
@@ -241,7 +271,9 @@ static boolean isNamedIn(AbstractMethodError e, Class<?> type) {
241271
final int paren = message.indexOf('(', methodStart);
242272
return message.charAt(name.length()) == '.'
243273
&& paren > methodStart
244-
&& message.indexOf('.', methodStart) < 0;
274+
&& message.indexOf('.', methodStart) < 0
275+
&& paren == methodStart + methodName.length()
276+
&& message.regionMatches(methodStart, methodName, 0, methodName.length());
245277
}
246278
return false;
247279
}

‎internal-api/src/test/java/datadog/trace/util/ClassLatchTest.java‎

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ void latchIfNamedLatchesOnlyWhenTheErrorNamesTheKey() {
172172
new ClassLatch<Object, String, RuntimeException>() {
173173
@Override
174174
protected String apply(Object target) {
175-
result[0] = latchIfNamed(target, receiverError(target.getClass()));
175+
result[0] = latchIfNamed(target, "m", receiverError(target.getClass()));
176176
return "named";
177177
}
178178
};
@@ -184,7 +184,7 @@ protected String apply(Object target) {
184184
new ClassLatch<Object, String, RuntimeException>() {
185185
@Override
186186
protected String apply(Object target) {
187-
result[0] = latchIfNamed(target, receiverError(Integer.class));
187+
result[0] = latchIfNamed(target, "m", receiverError(Integer.class));
188188
return "other";
189189
}
190190
};
@@ -193,35 +193,78 @@ protected String apply(Object target) {
193193
assertFalse(other.isLatched("x"));
194194
}
195195

196+
@Test
197+
void latchIfNamedDoesNotLatchWhenTheErrorNamesTheKeyButADifferentMethod() {
198+
// the receiver's own implementation of "m" can call some other method internally; an
199+
// AbstractMethodError raised by that other method still names the receiver class, but it is
200+
// not evidence that "m" itself is missing
201+
final boolean[] result = new boolean[1];
202+
ClassLatch<Object, String, RuntimeException> latch =
203+
new ClassLatch<Object, String, RuntimeException>() {
204+
@Override
205+
protected String apply(Object target) {
206+
result[0] =
207+
latchIfNamed(
208+
target,
209+
"m",
210+
new AbstractMethodError(
211+
"Receiver class "
212+
+ target.getClass().getName()
213+
+ " does not define or inherit an implementation of the resolved"
214+
+ " method 'abstract java.lang.String other()' of interface I."));
215+
return "named";
216+
}
217+
};
218+
latch.tryApplyOrNull("x");
219+
assertFalse(result[0]);
220+
assertFalse(latch.isLatched("x"));
221+
}
222+
196223
@Test
197224
void attributesTheHotSpotMessageFormats() {
198-
assertTrue(ClassLatch.isNamedIn(receiverError(String.class), String.class));
225+
assertTrue(ClassLatch.isNamedIn(receiverError(String.class), String.class, "m"));
226+
// the message names the receiver class, but blames a different method than "m"
227+
assertFalse(ClassLatch.isNamedIn(receiverError(String.class), String.class, "other"));
199228
// "java.lang.String" is a prefix of "java.lang.StringBuilder", but not the same class
200-
assertFalse(ClassLatch.isNamedIn(receiverError(String.class), StringBuilder.class));
229+
assertFalse(ClassLatch.isNamedIn(receiverError(String.class), StringBuilder.class, "m"));
201230
assertFalse(
202231
ClassLatch.isNamedIn(
203-
new AbstractMethodError("Receiver class " + String.class.getName()), String.class));
232+
new AbstractMethodError("Receiver class " + String.class.getName()),
233+
String.class,
234+
"m"));
204235

205236
// JDK 8 reports "<receiver class>.<method><descriptor>"
206237
String name = String.class.getName();
207238
assertTrue(
208239
ClassLatch.isNamedIn(
209240
new AbstractMethodError(name + ".getClientInfo()Ljava/util/Properties;"),
210-
String.class));
241+
String.class,
242+
"getClientInfo"));
243+
// the message names the receiver class, but blames a different method
244+
assertFalse(
245+
ClassLatch.isNamedIn(
246+
new AbstractMethodError(name + ".otherMethod()Ljava/util/Properties;"),
247+
String.class,
248+
"getClientInfo"));
211249
// a different class whose name merely starts with this one
212250
assertFalse(
213251
ClassLatch.isNamedIn(
214252
new AbstractMethodError(name + "Builder.getClientInfo()Ljava/util/Properties;"),
215-
String.class));
253+
String.class,
254+
"getClientInfo"));
216255
// a class in a package named like this class
217256
assertFalse(
218257
ClassLatch.isNamedIn(
219258
new AbstractMethodError(name + ".Inner.getClientInfo()Ljava/util/Properties;"),
220-
String.class));
221-
assertFalse(ClassLatch.isNamedIn(new AbstractMethodError(name), String.class));
222-
assertFalse(ClassLatch.isNamedIn(new AbstractMethodError(name + "."), String.class));
223-
assertFalse(ClassLatch.isNamedIn(new AbstractMethodError(), String.class));
224-
assertFalse(ClassLatch.isNamedIn(new AbstractMethodError("something else"), String.class));
259+
String.class,
260+
"getClientInfo"));
261+
assertFalse(ClassLatch.isNamedIn(new AbstractMethodError(name), String.class, "getClientInfo"));
262+
assertFalse(
263+
ClassLatch.isNamedIn(new AbstractMethodError(name + "."), String.class, "getClientInfo"));
264+
assertFalse(ClassLatch.isNamedIn(new AbstractMethodError(), String.class, "getClientInfo"));
265+
assertFalse(
266+
ClassLatch.isNamedIn(
267+
new AbstractMethodError("something else"), String.class, "getClientInfo"));
225268
}
226269

227270
@Test
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package datadog.trace.util;
2+
3+
import static java.util.Collections.singletonList;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import java.io.File;
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import javax.tools.JavaCompiler;
12+
13+
/** Shared support for tests that compile throwaway sources to pin real JVM error shapes. */
14+
final class CompilingClassLoaders {
15+
private CompilingClassLoaders() {}
16+
17+
static void compile(JavaCompiler compiler, Path out, Path classpath, String name, String source)
18+
throws IOException {
19+
Path file = out.resolve(name + ".java");
20+
Files.write(file, singletonList(source), StandardCharsets.UTF_8);
21+
int result =
22+
classpath == null
23+
? compiler.run(null, null, null, "-d", out.toString(), file.toString())
24+
: compiler.run(
25+
null,
26+
null,
27+
null,
28+
"-cp",
29+
classpath.toString() + File.pathSeparator,
30+
"-d",
31+
out.toString(),
32+
file.toString());
33+
assertEquals(0, result, "compiling " + name);
34+
}
35+
}

0 commit comments

Comments
 (0)