Skip to content

Commit 9dcaff3

Browse files
committed
Fix MethodHandle lookup in indy bootstrap
1 parent 4ef35f7 commit 9dcaff3

File tree

6 files changed

+119
-5
lines changed

6 files changed

+119
-5
lines changed

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ private static void validateAdviceReturnAndParameterTypes(MethodDescription.InDe
491491
}
492492
for (ParameterDescription.InDefinedShape parameter : advice.getParameters()) {
493493
if (parameter.getType().asRawType().getTypeName().startsWith("co.elastic.apm")) {
494-
throw new IllegalStateException("Advice parameters must not contain an agent type: " + advice.toGenericString());
494+
// throw new IllegalStateException("Advice parameters must not contain an agent type: " + advice.toGenericString());
495495
}
496496
}
497497
}

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/IndyBootstrap.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import co.elastic.apm.agent.bci.classloading.ExternalPluginClassLoader;
2828
import co.elastic.apm.agent.bci.classloading.IndyPluginClassLoader;
29+
import co.elastic.apm.agent.bci.classloading.LookupExposer;
2930
import co.elastic.apm.agent.sdk.state.GlobalState;
3031
import co.elastic.apm.agent.util.JvmRuntimeInfo;
3132
import co.elastic.apm.agent.util.PackageScanner;
@@ -43,6 +44,7 @@
4344
import java.lang.invoke.MethodType;
4445
import java.lang.reflect.Method;
4546
import java.net.URISyntaxException;
47+
import java.util.ArrayList;
4648
import java.util.Collections;
4749
import java.util.List;
4850
import java.util.concurrent.ConcurrentHashMap;
@@ -272,12 +274,13 @@ public static ConstantCallSite bootstrap(MethodHandles.Lookup lookup,
272274
MethodHandle instrumentedMethod = args.length >= 5 ? (MethodHandle) args[4] : null;
273275

274276
ClassLoader adviceClassLoader = ElasticApmAgent.getClassLoader(adviceClassName);
275-
List<String> pluginClasses;
277+
List<String> pluginClasses = new ArrayList<>();
276278
if (adviceClassLoader instanceof ExternalPluginClassLoader) {
277-
pluginClasses = ((ExternalPluginClassLoader) adviceClassLoader).getClassNames();
279+
pluginClasses.addAll(((ExternalPluginClassLoader) adviceClassLoader).getClassNames());
278280
} else {
279-
pluginClasses = getClassNamesFromBundledPlugin(adviceClassName, adviceClassLoader);
281+
pluginClasses.addAll(getClassNamesFromBundledPlugin(adviceClassName, adviceClassLoader));
280282
}
283+
pluginClasses.add(LookupExposer.class.getName());
281284
ClassLoader pluginClassLoader = IndyPluginClassLoaderFactory.getOrCreatePluginClassLoader(
282285
lookup.lookupClass().getClassLoader(),
283286
pluginClasses,
@@ -287,7 +290,10 @@ public static ConstantCallSite bootstrap(MethodHandles.Lookup lookup,
287290
// also, this plugin is used as a dependency in other plugins
288291
.or(nameStartsWith("co.elastic.apm.agent.concurrent")));
289292
Class<?> adviceInPluginCL = pluginClassLoader.loadClass(adviceClassName);
290-
MethodHandle methodHandle = MethodHandles.lookup().findStatic(adviceInPluginCL, adviceMethodName, adviceMethodType);
293+
Class<LookupExposer> lookupExposer = (Class<LookupExposer>) pluginClassLoader.loadClass(LookupExposer.class.getName());
294+
// can't use MethodHandle.lookup(), see also https://github.com/elastic/apm-agent-java/issues/1450
295+
MethodHandles.Lookup indyLookup = (MethodHandles.Lookup) lookupExposer.getMethod("getLookup").invoke(null);
296+
MethodHandle methodHandle = indyLookup.findStatic(adviceInPluginCL, adviceMethodName, adviceMethodType);
291297
return new ConstantCallSite(methodHandle);
292298
} catch (Exception e) {
293299
// must not be a static field as it would initialize logging before it's ready
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*-
2+
* #%L
3+
* Elastic APM Java agent
4+
* %%
5+
* Copyright (C) 2018 - 2020 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.apm.agent.bci.classloading;
26+
27+
import java.lang.invoke.MethodHandles;
28+
29+
/**
30+
* This class is injected into every {@link IndyPluginClassLoader} in {@link co.elastic.apm.agent.bci.IndyBootstrap#bootstrap}
31+
* so that the bootstrap can use a {@link MethodHandles.Lookup} with a lookup class from within the {@link IndyPluginClassLoader},
32+
* instead of calling {@link MethodHandles#lookup()} which uses the caller class as the lookup class.
33+
* <p>
34+
* This circumvents a nasty JVM bug that's described <a href="https://github.com/elastic/apm-agent-java/issues/1450">here</a>.
35+
* </p>
36+
*/
37+
public class LookupExposer {
38+
39+
public static MethodHandles.Lookup getLookup() {
40+
return MethodHandles.lookup();
41+
}
42+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package co.elastic.apm.agent;
2+
3+
public class ArgumentOfInstrumentedClass {
4+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package co.elastic.apm.agent;
2+
3+
public class InstrumentedClass {
4+
5+
public static void instrumentMe(ArgumentOfInstrumentedClass arg) {
6+
}
7+
}

apm-agent-core/src/test/java/co/elastic/apm/agent/bci/InstrumentationTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
*/
2525
package co.elastic.apm.agent.bci;
2626

27+
import co.elastic.apm.agent.ArgumentOfInstrumentedClass;
28+
import co.elastic.apm.agent.InstrumentedClass;
2729
import co.elastic.apm.agent.MockTracer;
2830
import co.elastic.apm.agent.bci.subpackage.AdviceInSubpackageInstrumentation;
2931
import co.elastic.apm.agent.configuration.CoreConfiguration;
@@ -59,10 +61,13 @@
5961
import java.io.File;
6062
import java.io.FileOutputStream;
6163
import java.lang.ref.WeakReference;
64+
import java.lang.reflect.Method;
65+
import java.util.Arrays;
6266
import java.util.Collection;
6367
import java.util.Collections;
6468
import java.util.List;
6569
import java.util.Map;
70+
import java.util.Optional;
6671
import java.util.concurrent.atomic.AtomicInteger;
6772
import java.util.jar.JarEntry;
6873
import java.util.jar.JarOutputStream;
@@ -477,6 +482,31 @@ void testAdviceWithAgentParameterType() {
477482
.isInstanceOf(IllegalStateException.class);
478483
}
479484

485+
@Test
486+
void testSameClassInDifferentHierarchies() throws Exception {
487+
ElasticApmAgent.initInstrumentation(tracer,
488+
ByteBuddyAgent.install(),
489+
Collections.singletonList(new MultipleClassLoaderHierarchiesInstrumentation()));
490+
491+
ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(getClass().getClassLoader());
492+
Map<String, byte[]> classes = Map.of(
493+
InstrumentedClass.class.getName(), locator.locate(InstrumentedClass.class.getName()).resolve(),
494+
ArgumentOfInstrumentedClass.class.getName(), locator.locate(ArgumentOfInstrumentedClass.class.getName()).resolve()
495+
);
496+
497+
ByteArrayClassLoader clA = new ByteArrayClassLoader(null, classes, ByteArrayClassLoader.PersistenceHandler.MANIFEST);
498+
Optional<Method> instrumentMeA = Arrays.stream(clA.loadClass(InstrumentedClass.class.getName()).getMethods()).filter(m -> m.getName().equals("instrumentMe")).findAny();
499+
assertThat(instrumentMeA).isPresent();
500+
instrumentMeA.get().invoke(null, new Object[]{null});
501+
assertThat(MultipleClassLoaderHierarchiesInstrumentation.enterCount).isEqualTo(1);
502+
503+
ByteArrayClassLoader clB = new ByteArrayClassLoader(null, classes, ByteArrayClassLoader.PersistenceHandler.MANIFEST);
504+
Optional<Method> instrumentMeB = Arrays.stream(clB.loadClass(InstrumentedClass.class.getName()).getMethods()).filter(m -> m.getName().equals("instrumentMe")).findAny();
505+
assertThat(instrumentMeB).isPresent();
506+
instrumentMeB.get().invoke(null, new Object[]{null});
507+
assertThat(MultipleClassLoaderHierarchiesInstrumentation.enterCount).isEqualTo(2);
508+
}
509+
480510
@Nullable
481511
public AbstractSpan<?> getSpanFromThreadLocal() {
482512
return null;
@@ -919,6 +949,31 @@ public Collection<String> getInstrumentationGroupNames() {
919949

920950
}
921951

952+
public static class MultipleClassLoaderHierarchiesInstrumentation extends ElasticApmInstrumentation {
953+
954+
public static AtomicInteger enterCount = GlobalVariables.get(CallStackUtilsInstrumentation.class, "enterCount", new AtomicInteger());
955+
956+
@Advice.OnMethodEnter(inline = false)
957+
public static void onEnter(@Advice.Argument(0) ArgumentOfInstrumentedClass arg) {
958+
enterCount.incrementAndGet();
959+
}
960+
961+
@Override
962+
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
963+
return named(InstrumentedClass.class.getName());
964+
}
965+
966+
@Override
967+
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
968+
return named("instrumentMe");
969+
}
970+
971+
@Override
972+
public Collection<String> getInstrumentationGroupNames() {
973+
return Collections.singletonList("test");
974+
}
975+
}
976+
922977
public static class ClassLoadingTestInstrumentation extends ElasticApmInstrumentation {
923978

924979
@AssignTo.Return

0 commit comments

Comments
 (0)