Skip to content

Commit

Permalink
Override IndyBootstrapDispatcher class module (#1468)
Browse files Browse the repository at this point in the history
  • Loading branch information
eyalkoren authored Nov 5, 2020
1 parent c279752 commit 953f8dc
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ endif::[]
* Fix ability to disable agent on startup wasn't working for runtime attach {pull}1444[1444]
* Avoid `UnsupportedOperationException` on some spring application startup {pull}1464[1464]
* Fix ignored runtime attach `config_file` {pull}1469[1469]
* Fix `IllegalAccessError: Module 'java.base' no access to: package 'java.lang'...` in J9 VMs of Java version >= 9 -
{pull}1468[#1468]
===== Refactors
* Migrate some plugins to indy dispatcher {pull}1404[1404] {pull}1411[1411]
Expand Down
8 changes: 7 additions & 1 deletion apm-agent-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>co.elastic.apm</groupId>
<artifactId>apm-agent-parent</artifactId>
<groupId>co.elastic.apm</groupId>
<version>1.18.2-SNAPSHOT</version>
</parent>

Expand Down Expand Up @@ -72,6 +72,12 @@
<version>2.1.11</version>
</dependency>

<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-indy-bootstrap-module</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ private static AgentBuilder.Transformer.ForAdvice getTransformer(final ElasticAp
if (!(instrumentation instanceof TracerAwareInstrumentation)
|| ((TracerAwareInstrumentation) instrumentation).indyPlugin()) {
validateAdvice(instrumentation.getAdviceClass());
withCustomMapping = withCustomMapping.bootstrap(IndyBootstrap.getIndyBootstrapMethod());
withCustomMapping = withCustomMapping.bootstrap(IndyBootstrap.getIndyBootstrapMethod(logger));
}
return new AgentBuilder.Transformer.ForAdvice(withCustomMapping)
.advice(new ElementMatcher<MethodDescription>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
import co.elastic.apm.agent.util.PackageScanner;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.dynamic.loading.ClassInjector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.stagemonitor.util.IOUtils;
import sun.misc.Unsafe;

import javax.annotation.Nullable;
import java.io.IOException;
Expand Down Expand Up @@ -166,9 +168,14 @@ public class IndyBootstrap {
* Starts with {@code java.lang} so that OSGi class loaders don't restrict access to it
*/
private static final String INDY_BOOTSTRAP_CLASS_NAME = "java.lang.IndyBootstrapDispatcher";
/**
* Needs to be loaded from the bootstrap CL because it uses {@link Unsafe}.
* In addition, needs to be loaded explicitly by name only when running on Java 9, because compiled with Java 9
*/
private static final String INDY_BOOTSTRAP_MODULE_SETTER_CLASS_NAME = "co.elastic.apm.agent.bci.IndyBootstrapDispatcherModuleSetter";
/**
* The class file of {@code java.lang.IndyBootstrapDispatcher}.
* Ends with {@code clazz} because if it ended with {@code clazz}, it would be loaded like a regular class.
* Ends with {@code clazz} because if it ended with {@code class}, it would be loaded like a regular class.
*/
private static final String INDY_BOOTSTRAP_RESOURCE = "bootstrap/IndyBootstrapDispatcher.clazz";
/**
Expand All @@ -178,12 +185,12 @@ public class IndyBootstrap {
@Nullable
static Method indyBootstrapMethod;

public static Method getIndyBootstrapMethod() {
public static Method getIndyBootstrapMethod(final Logger logger) {
if (indyBootstrapMethod != null) {
return indyBootstrapMethod;
}
try {
Class<?> indyBootstrapClass = initIndyBootstrap();
Class<?> indyBootstrapClass = initIndyBootstrap(logger);
indyBootstrapClass
.getField("bootstrap")
.set(null, IndyBootstrap.class.getMethod("bootstrap", MethodHandles.Lookup.class, String.class, MethodType.class, Object[].class));
Expand All @@ -196,14 +203,40 @@ public static Method getIndyBootstrapMethod() {
/**
* Injects the {@code java.lang.IndyBootstrapDispatcher} class into the bootstrap class loader if it wasn't already.
*/
private static Class<?> initIndyBootstrap() throws Exception {
private static Class<?> initIndyBootstrap(final Logger logger) throws Exception {
Class<?> indyBootstrapDispatcherClass;
try {
return Class.forName(INDY_BOOTSTRAP_CLASS_NAME, false, null);
indyBootstrapDispatcherClass = Class.forName(INDY_BOOTSTRAP_CLASS_NAME, false, null);
} catch (ClassNotFoundException e) {
byte[] bootstrapClass = IOUtils.readToBytes(ClassLoader.getSystemClassLoader().getResourceAsStream(INDY_BOOTSTRAP_RESOURCE));
ClassInjector.UsingUnsafe.ofBootLoader().injectRaw(Collections.singletonMap(INDY_BOOTSTRAP_CLASS_NAME, bootstrapClass));
indyBootstrapDispatcherClass = Class.forName(INDY_BOOTSTRAP_CLASS_NAME, false, null);
}

if (JvmRuntimeInfo.getMajorVersion() >= 9 && JvmRuntimeInfo.isJ9VM()) {
try {
logger.info("Overriding IndyBootstrapDispatcher class's module to java.base module. This is required in J9 VMs.");
setJavaBaseModule(indyBootstrapDispatcherClass);
} catch (Throwable throwable) {
logger.warn("Failed to setup proper module for the IndyBootstrapDispatcher class, instrumentation may fail", throwable);
}
}
return Class.forName(INDY_BOOTSTRAP_CLASS_NAME, false, null);
return indyBootstrapDispatcherClass;
}

/**
* A package-private method for unit-testing of the module overriding functionality
*
* @param targetClass class for which module should be overridden with the {@code java.base} module
* @throws Throwable in case of any failure related to module overriding
*/
static void setJavaBaseModule(Class<?> targetClass) throws Throwable {
// In order to override the original unnamed module assigned to the IndyBootstrapDispatcher, we rely on the
// Unsafe API, which requires the caller to be loaded by the Bootstrap CL
Class<?> moduleSetterClass = Class.forName(INDY_BOOTSTRAP_MODULE_SETTER_CLASS_NAME, false, null);
MethodHandles.lookup()
.findStatic(moduleSetterClass, "setJavaBaseModule", MethodType.methodType(void.class, Class.class))
.invoke(targetClass);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class JvmRuntimeInfo {
private static int updateVersion;
private static boolean isHotSpot;
private static boolean isIbmJ9;
private static boolean isJ9;

static {
parseVmInfo(System.getProperty("java.version"), System.getProperty("java.vm.name"), System.getProperty("java.vm.version"));
Expand Down Expand Up @@ -91,6 +92,7 @@ static void parseVmInfo(String version, String vmName, @Nullable String vmVersio

isHotSpot = vmName.contains("HotSpot(TM)") || vmName.contains("OpenJDK");
isIbmJ9 = vmName.contains("IBM J9");
isJ9 = vmName.contains("J9");
}

public static String getJavaVersion() {
Expand All @@ -110,6 +112,10 @@ public static int getMajorVersion() {
return majorVersion;
}

public static boolean isJ9VM() {
return isJ9;
}

/**
* Checks if a given version of the JVM is likely supported by this agent.
* <br>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 - 2020 Elastic and contributors
* %%
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* #L%
*/
package co.elastic.apm.agent.bci;

import co.elastic.apm.agent.AbstractInstrumentationTest;
import net.bytebuddy.dynamic.loading.ClassInjector;
import org.junit.jupiter.api.Test;
import org.stagemonitor.util.IOUtils;

import java.io.InputStream;
import java.util.Collections;

import static org.assertj.core.api.Assertions.assertThat;

class IndyBootstrapTest extends AbstractInstrumentationTest {

@Test
void testSetJavaBaseModule() throws Throwable {
Module javaBaseModule = Class.class.getModule();
assertThat(IndyBootstrapTest.class.getModule()).isNotEqualTo(javaBaseModule);

// In order to test this functionality, IndyBootstrapDispatcherModuleSetter needs to be loaded from the Boot CL.
// We don't mind loading it with the test's class loader as well only to get it's class file
InputStream classFileAsStream = IndyBootstrapDispatcherModuleSetter.class.getResourceAsStream("IndyBootstrapDispatcherModuleSetter.class");
byte[] bootstrapClass = IOUtils.readToBytes(classFileAsStream);
ClassInjector.UsingUnsafe.ofBootLoader().injectRaw(Collections.singletonMap(IndyBootstrapDispatcherModuleSetter.class.getName(), bootstrapClass));

IndyBootstrap.setJavaBaseModule(IndyBootstrapTest.class);
assertThat(IndyBootstrapTest.class.getModule()).isEqualTo(javaBaseModule);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
package co.elastic.apm.agent.httpclient;

import co.elastic.apm.agent.impl.transaction.Span;
import co.elastic.apm.agent.util.JvmRuntimeInfo;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.BooleanMatcher;
import net.bytebuddy.matcher.ElementMatcher;

import javax.annotation.Nullable;
Expand All @@ -44,7 +46,7 @@ public class HttpClientInstrumentation extends AbstractHttpClientInstrumentation

@Override
public ElementMatcher<? super NamedElement> getTypeMatcherPreFilter() {
return nameContains("HttpClient");
return new BooleanMatcher<>(JvmRuntimeInfo.getMajorVersion() >= 11).and(nameContains("HttpClient"));
}

@Override
Expand Down
26 changes: 26 additions & 0 deletions apm-indy-bootstrap-module/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>apm-agent-parent</artifactId>
<groupId>co.elastic.apm</groupId>
<version>1.18.2-SNAPSHOT</version>
</parent>

<artifactId>apm-indy-bootstrap-module</artifactId>
<name>${project.groupId}:${project.artifactId}</name>

<properties>
<apm-agent-parent.base.dir>${project.basedir}/..</apm-agent-parent.base.dir>

<maven.compiler.target>9</maven.compiler.target>

<!-- using release to enforce java 9 API (animal sniffer not supported) -->
<maven.compiler.release>${maven.compiler.target}</maven.compiler.release>

<!-- not supported/relevant beyond java9, see https://github.com/mojohaus/animal-sniffer/issues/62
now enforced through the maven.compiler.release property -->
<animal.sniffer.skip>true</animal.sniffer.skip>
</properties>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 - 2020 Elastic and contributors
* %%
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* #L%
*/
package co.elastic.apm.agent.bci;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
* Overrides the module assigned to the class in with the {@code java.base} module.
* <p>
* NOTE: This class is compiled with Java 9 so it must only be loaded though reflection and only when running on Java 9.
* In addition, since it relies on the {@link Unsafe} API, it must be loaded by the bootstrap or platform class loaders.
*/
@SuppressWarnings("unused")
public class IndyBootstrapDispatcherModuleSetter {

public static void setJavaBaseModule(Class<?> indyBootstrapDispatcherClass) throws Exception {
Field moduleField = Class.class.getDeclaredField("module");
if (moduleField.getType() == Module.class) {
Module javaBaseModule = Class.class.getModule();
Unsafe unsafe = Unsafe.getUnsafe();
unsafe.putObject(indyBootstrapDispatcherClass, unsafe.objectFieldOffset(moduleField), javaBaseModule);
} else {
throw new IllegalStateException("Unexpected module field type: " + moduleField.getType().getName());
}
}
}
5 changes: 5 additions & 0 deletions elastic-apm-agent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<artifactId>apm-agent-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-indy-bootstrap-module</artifactId>
<version>${project.version}</version>
</dependency>

<!--Agent plugins, sorted alphabetically -->
<dependency>
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<module>integration-tests</module>
<module>apm-agent-attach</module>
<module>apm-agent-plugin-sdk</module>
<module>apm-indy-bootstrap-module</module>
</modules>

<properties>
Expand Down

0 comments on commit 953f8dc

Please sign in to comment.