Skip to content

Commit

Permalink
Agent can inject class into Java 9+ bootstrap class loader (bazelbuil…
Browse files Browse the repository at this point in the history
  • Loading branch information
Godin authored and marchof committed Jan 21, 2019
1 parent 519226e commit d0a0577
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 1 deletion.
2 changes: 1 addition & 1 deletion jacoco/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
<configuration>
<rules>
<requireFilesSize>
<maxsize>4300000</maxsize>
<maxsize>4400000</maxsize>
<minsize>3400000</minsize>
<files>
<file>${project.build.directory}/jacoco-${qualified.bundle.version}.zip</file>
Expand Down
55 changes: 55 additions & 0 deletions org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/PreMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
package org.jacoco.agent.rt.internal;

import java.lang.instrument.Instrumentation;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.jacoco.core.runtime.AgentOptions;
import org.jacoco.core.runtime.IRuntime;
import org.jacoco.core.runtime.InjectedClassRuntime;
import org.jacoco.core.runtime.ModifiedSystemClassRuntime;

/**
Expand Down Expand Up @@ -52,7 +56,58 @@ public static void premain(final String options, final Instrumentation inst)

private static IRuntime createRuntime(final Instrumentation inst)
throws Exception {

if (redefineJavaBaseModule(inst)) {
return new InjectedClassRuntime(Object.class, "$JaCoCo");
}

return ModifiedSystemClassRuntime.createFor(inst, "java/lang/UnknownError");
}

/**
* Opens {@code java.base} module for {@link InjectedClassRuntime} when
* executed on Java 9 JREs or higher.
*
* @return <code>true</code> when running on Java 9 or higher,
* <code>false</code> otherwise
* @throws Exception
* if unable to open
*/
private static boolean redefineJavaBaseModule(
final Instrumentation instrumentation) throws Exception {
try {
Class.forName("java.lang.Module");
} catch (final ClassNotFoundException e) {
return false;
}

Instrumentation.class.getMethod("redefineModule", //
Class.forName("java.lang.Module"), //
Set.class, //
Map.class, //
Map.class, //
Set.class, //
Map.class //
).invoke(instrumentation, // instance
getModule(Object.class), // module
Collections.emptySet(), // extraReads
Collections.emptyMap(), // extraExports
Collections.singletonMap("java.lang",
Collections.singleton(
getModule(InjectedClassRuntime.class))), // extraOpens
Collections.emptySet(), // extraUses
Collections.emptyMap() // extraProvides
);
return true;
}

/**
* @return {@code cls.getModule()}
*/
private static Object getModule(final Class<?> cls) throws Exception {
return Class.class //
.getMethod("getModule") //
.invoke(cls);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*******************************************************************************
* Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Evgeny Mandrikov - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.runtime;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.InvocationTargetException;

import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.internal.AssumptionViolatedException;
import org.junit.rules.TestName;

/**
* Unit test for {@link InjectedClassRuntime}.
*/
public class InjectedClassRuntimeTest extends RuntimeTestBase {

@Rule
public TestName testName = new TestName();

@BeforeClass
public static void requires_at_least_Java_9() {
try {
Class.forName("java.lang.Module");
} catch (final ClassNotFoundException e) {
throw new AssumptionViolatedException(
"this test requires at least Java 9");
}
}

@Override
public IRuntime createRuntime() {
return new InjectedClassRuntime(InjectedClassRuntimeTest.class,
testName.getMethodName());
}

@Test
public void startup_should_not_create_duplicate_class_definition()
throws Exception {
try {
createRuntime().startup(null);
fail("exception expected");
} catch (final InvocationTargetException e) {
assertTrue(e.getCause() instanceof LinkageError);
assertTrue(e.getCause().getMessage()
.contains("duplicate class definition"));
}
}

}
141 changes: 141 additions & 0 deletions org.jacoco.core/src/org/jacoco/core/runtime/InjectedClassRuntime.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*******************************************************************************
* Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Evgeny Mandrikov - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.runtime;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
* {@link IRuntime} which defines a new class using
* {@code java.lang.invoke.MethodHandles.Lookup.defineClass} introduced in Java
* 9. Module where class will be defined must be opened to at least module of
* this class.
*/
public class InjectedClassRuntime extends AbstractRuntime {

private static final String FIELD_NAME = "data";

private static final String FIELD_TYPE = "Ljava/lang/Object;";

private final Class<?> locator;

private final String injectedClassName;

/**
* Creates a new runtime which will define a class to the same class loader
* and in the same package and protection domain as given class.
*
* @param locator
* class to identify the target class loader and package
* @param simpleClassName
* simple name of the class to be defined
*/
public InjectedClassRuntime(final Class<?> locator,
final String simpleClassName) {
this.locator = locator;
this.injectedClassName = locator.getPackage().getName().replace('.',
'/') + '/' + simpleClassName;
}

@Override
public void startup(final RuntimeData data) throws Exception {
super.startup(data);
Lookup //
.privateLookupIn(locator, Lookup.lookup()) //
.defineClass(createClass(injectedClassName)) //
.getField(FIELD_NAME) //
.set(null, data);
}

public void shutdown() {
// nothing to do
}

public int generateDataAccessor(final long classid, final String classname,
final int probecount, final MethodVisitor mv) {
mv.visitFieldInsn(Opcodes.GETSTATIC, injectedClassName, FIELD_NAME,
FIELD_TYPE);

RuntimeData.generateAccessCall(classid, classname, probecount, mv);

return 6;
}

private static byte[] createClass(final String name) {
final ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V9, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC,
name.replace('.', '/'), null, "java/lang/Object", null);
cw.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, FIELD_NAME,
FIELD_TYPE, null, null);
cw.visitEnd();
return cw.toByteArray();
}

/**
* Provides access to classes {@code java.lang.invoke.MethodHandles} and
* {@code java.lang.invoke.MethodHandles.Lookup} introduced in Java 8.
*/
private static class Lookup {

private final Object instance;

private Lookup(final Object instance) {
this.instance = instance;
}

/**
* @return a lookup object for the caller of this method
*/
static Lookup lookup() throws Exception {
return new Lookup(Class //
.forName("java.lang.invoke.MethodHandles") //
.getMethod("lookup") //
.invoke(null));
}

/**
* See corresponding method introduced in Java 9.
*
* @param targetClass
* the target class
* @param lookup
* the caller lookup object
* @return a lookup object for the target class, with private access
*/
static Lookup privateLookupIn(final Class<?> targetClass,
final Lookup lookup) throws Exception {
return new Lookup(Class //
.forName("java.lang.invoke.MethodHandles") //
.getMethod("privateLookupIn", Class.class,
Class.forName(
"java.lang.invoke.MethodHandles$Lookup")) //
.invoke(null, targetClass, lookup.instance));
}

/**
* See corresponding method introduced in Java 9.
*
* @param bytes
* the class bytes
* @return class
*/
Class<?> defineClass(final byte[] bytes) throws Exception {
return (Class<?>) Class //
.forName("java.lang.invoke.MethodHandles$Lookup")
.getMethod("defineClass", byte[].class)
.invoke(this.instance, new Object[] { bytes });
}

}

}
2 changes: 2 additions & 0 deletions org.jacoco.doc/docroot/doc/changes.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ <h3>New Features</h3>
(GitHub <a href="https://github.com/jacoco/jacoco/issues/819">#819</a>).</li>
<li>Empty class and sourcefile nodes are preserved and available in XML report
(GitHub <a href="https://github.com/jacoco/jacoco/issues/817">#817</a>).</li>
<li>Agent avoids conflicts with other agents when running on Java 9+
(GitHub <a href="https://github.com/jacoco/jacoco/issues/829">#829</a>).</li>
</ul>

<h3>Fixed Bugs</h3>
Expand Down

0 comments on commit d0a0577

Please sign in to comment.