Skip to content

Commit b865890

Browse files
Godinmarchof
andauthored
Agent should not open java.lang package to unnamed module of the application class loader (#1334)
Co-authored-by: Evgeny Mandrikov <[email protected]> Co-authored-by: Marc R. Hoffmann <[email protected]>
1 parent 5bc2fae commit b865890

7 files changed

Lines changed: 300 additions & 56 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors
3+
* This program and the accompanying materials are made available under
4+
* the terms of the Eclipse Public License 2.0 which is available at
5+
* http://www.eclipse.org/legal/epl-2.0
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Evgeny Mandrikov - initial API and implementation
11+
*
12+
*******************************************************************************/
13+
package org.jacoco.agent.rt.internal;
14+
15+
import static org.junit.Assert.assertEquals;
16+
import static org.junit.Assert.assertNotSame;
17+
import static org.junit.Assert.assertSame;
18+
19+
import org.jacoco.core.test.validation.JavaVersion;
20+
import org.junit.Test;
21+
22+
/**
23+
* Unit tests for {@link AgentModule}.
24+
*/
25+
public class AgentModuleTest {
26+
27+
@Test
28+
public void isSupported_should_return_false_before_Java9() {
29+
Boolean expected = Boolean
30+
.valueOf(!JavaVersion.current().isBefore("9"));
31+
Boolean supported = Boolean.valueOf(AgentModule.isSupported());
32+
assertEquals(expected, supported);
33+
}
34+
35+
@Test
36+
public void should_only_load_classes_in_scope() throws Exception {
37+
AgentModule am = new AgentModule();
38+
Class<? extends Target> targetclass = am
39+
.loadClassInModule(TargetImpl.class);
40+
Target t = targetclass.getDeclaredConstructor().newInstance();
41+
42+
assertNotSame(this.getClass().getClassLoader(),
43+
t.getClass().getClassLoader());
44+
assertSame(t.getClass().getClassLoader(),
45+
t.getInnerClassInstance().getClass().getClassLoader());
46+
assertNotSame(this.getClass().getClassLoader(),
47+
t.getInnerClassInstance().getClass().getClassLoader());
48+
assertSame(this.getClass().getClassLoader(),
49+
t.getOtherClassInstance().getClass().getClassLoader());
50+
}
51+
52+
public interface Target {
53+
54+
Object getInnerClassInstance();
55+
56+
Object getOtherClassInstance();
57+
58+
}
59+
60+
public static class TargetImpl implements Target {
61+
62+
static class Inner {
63+
}
64+
65+
public Object getInnerClassInstance() {
66+
return new Inner();
67+
}
68+
69+
public Object getOtherClassInstance() {
70+
return new Other();
71+
}
72+
}
73+
74+
public static class Other {
75+
}
76+
77+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors
3+
* This program and the accompanying materials are made available under
4+
* the terms of the Eclipse Public License 2.0 which is available at
5+
* http://www.eclipse.org/legal/epl-2.0
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Evgeny Mandrikov - initial API and implementation
11+
* Marc R. Hoffmann - move to separate class
12+
*
13+
*******************************************************************************/
14+
package org.jacoco.agent.rt.internal;
15+
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.lang.instrument.Instrumentation;
19+
import java.util.Collections;
20+
import java.util.HashSet;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import org.jacoco.core.internal.InputStreams;
25+
26+
/**
27+
* An isolated class loader and distinct module to encapsulate JaCoCo runtime
28+
* classes. This isolated environment allows to specifically open JDK packages
29+
* to the agent runtime without changing package accessibility for the
30+
* application under test.
31+
* <p>
32+
* The implementation uses the property that the <a href=
33+
* "https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-5.html#jvms-5.3.6">
34+
* unnamed module is distinct from all run-time modules (including unnamed
35+
* modules) bound to other class loaders</a>.
36+
*/
37+
public class AgentModule {
38+
39+
/**
40+
* Checks whether Java modules are supported by the current Java runtime.
41+
*
42+
* @return <code>true</code> is modules are supported
43+
*/
44+
public static boolean isSupported() {
45+
try {
46+
getModuleClass();
47+
} catch (final ClassNotFoundException e) {
48+
return false;
49+
}
50+
return true;
51+
}
52+
53+
private final Set<String> scope = new HashSet<String>();
54+
private final ClassLoader classLoader;
55+
56+
/**
57+
* Creates a new isolated module.
58+
*
59+
* @throws Exception
60+
* if it cannot be created
61+
*/
62+
public AgentModule() throws Exception {
63+
classLoader = new ClassLoader() {
64+
@Override
65+
protected Class<?> loadClass(final String name,
66+
final boolean resolve) throws ClassNotFoundException {
67+
if (!scope.contains(name)) {
68+
return super.loadClass(name, resolve);
69+
}
70+
final InputStream resourceAsStream = getResourceAsStream(
71+
name.replace('.', '/') + ".class");
72+
final byte[] bytes;
73+
try {
74+
bytes = InputStreams.readFully(resourceAsStream);
75+
} catch (final IOException e) {
76+
throw new RuntimeException(e);
77+
}
78+
return defineClass(name, bytes, 0, bytes.length);
79+
}
80+
};
81+
}
82+
83+
/**
84+
* Opens the package of the provided class to the module created in this
85+
* {@link #AgentModule()} instance.
86+
*
87+
* @param instrumentation
88+
* service provided to the agent by the Java runtime
89+
* @param classInPackage
90+
* example class of the package to open
91+
* @throws Exception
92+
* if package cannot be opened
93+
*/
94+
public void openPackage(final Instrumentation instrumentation,
95+
final Class<?> classInPackage) throws Exception {
96+
97+
// module of the package to open
98+
final Object module = Class.class.getMethod("getModule")
99+
.invoke(classInPackage);
100+
101+
// unnamed module of our classloader
102+
final Object unnamedModule = ClassLoader.class
103+
.getMethod("getUnnamedModule").invoke(classLoader);
104+
105+
// Open package java.lang to the unnamed module of our class loader
106+
Instrumentation.class.getMethod("redefineModule", //
107+
getModuleClass(), //
108+
Set.class, //
109+
Map.class, //
110+
Map.class, //
111+
Set.class, //
112+
Map.class //
113+
).invoke(instrumentation, // instance
114+
module, // module
115+
Collections.emptySet(), // extraReads
116+
Collections.emptyMap(), // extraExports
117+
Collections.singletonMap(classInPackage.getPackage().getName(),
118+
Collections.singleton(unnamedModule)), // extraOpens
119+
Collections.emptySet(), // extraUses
120+
Collections.emptyMap() // extraProvides
121+
);
122+
}
123+
124+
/**
125+
* Loads a copy of the given class in the isolated classloader. Also any
126+
* inner classes are loader from the isolated classloader.
127+
*
128+
* @param <T>
129+
* type of the class to load
130+
* @param original
131+
* original class definition which is used as source
132+
* @return class object from the isolated class loader
133+
* @throws Exception
134+
* if the class cannot be loaded
135+
*/
136+
@SuppressWarnings("unchecked")
137+
public <T> Class<T> loadClassInModule(final Class<T> original)
138+
throws Exception {
139+
addToScopeWithInnerClasses(original);
140+
return (Class<T>) classLoader.loadClass(original.getName());
141+
}
142+
143+
private void addToScopeWithInnerClasses(final Class<?> c) {
144+
scope.add(c.getName());
145+
for (final Class<?> i : c.getDeclaredClasses()) {
146+
addToScopeWithInnerClasses(i);
147+
}
148+
}
149+
150+
private static Class<?> getModuleClass() throws ClassNotFoundException {
151+
return Class.forName("java.lang.Module");
152+
}
153+
154+
}

org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/PreMain.java

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
package org.jacoco.agent.rt.internal;
1414

1515
import java.lang.instrument.Instrumentation;
16-
import java.util.Collections;
17-
import java.util.Map;
18-
import java.util.Set;
1916

2017
import org.jacoco.core.runtime.AgentOptions;
2118
import org.jacoco.core.runtime.IRuntime;
@@ -58,58 +55,17 @@ public static void premain(final String options, final Instrumentation inst)
5855
private static IRuntime createRuntime(final Instrumentation inst)
5956
throws Exception {
6057

61-
if (redefineJavaBaseModule(inst)) {
62-
return new InjectedClassRuntime(Object.class, "$JaCoCo");
58+
if (AgentModule.isSupported()) {
59+
final AgentModule module = new AgentModule();
60+
module.openPackage(inst, Object.class);
61+
final Class<InjectedClassRuntime> clazz = module
62+
.loadClassInModule(InjectedClassRuntime.class);
63+
return clazz.getConstructor(Class.class, String.class)
64+
.newInstance(Object.class, "$JaCoCo");
6365
}
6466

6567
return ModifiedSystemClassRuntime.createFor(inst,
6668
"java/lang/UnknownError");
6769
}
6870

69-
/**
70-
* Opens {@code java.base} module for {@link InjectedClassRuntime} when
71-
* executed on Java 9 JREs or higher.
72-
*
73-
* @return <code>true</code> when running on Java 9 or higher,
74-
* <code>false</code> otherwise
75-
* @throws Exception
76-
* if unable to open
77-
*/
78-
private static boolean redefineJavaBaseModule(
79-
final Instrumentation instrumentation) throws Exception {
80-
try {
81-
Class.forName("java.lang.Module");
82-
} catch (final ClassNotFoundException e) {
83-
return false;
84-
}
85-
86-
Instrumentation.class.getMethod("redefineModule", //
87-
Class.forName("java.lang.Module"), //
88-
Set.class, //
89-
Map.class, //
90-
Map.class, //
91-
Set.class, //
92-
Map.class //
93-
).invoke(instrumentation, // instance
94-
getModule(Object.class), // module
95-
Collections.emptySet(), // extraReads
96-
Collections.emptyMap(), // extraExports
97-
Collections.singletonMap("java.lang",
98-
Collections.singleton(
99-
getModule(InjectedClassRuntime.class))), // extraOpens
100-
Collections.emptySet(), // extraUses
101-
Collections.emptyMap() // extraProvides
102-
);
103-
return true;
104-
}
105-
106-
/**
107-
* @return {@code cls.getModule()}
108-
*/
109-
private static Object getModule(final Class<?> cls) throws Exception {
110-
return Class.class //
111-
.getMethod("getModule") //
112-
.invoke(cls);
113-
}
114-
11571
}

org.jacoco.ant.test/src/org/jacoco/ant/CoverageTaskTest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,12 @@
158158
<au:assertLogContains text="java/sql/Timestamp"/>
159159
</target>
160160

161+
<target name="testIllegalReflectiveAccess">
162+
<jacoco:coverage destfile="${exec.file}">
163+
<java classname="org.jacoco.ant.IllegalReflectiveAccessTarget" fork="true" failonerror="true">
164+
<classpath path="${org.jacoco.ant.coverageTaskTest.classes.dir}"/>
165+
</java>
166+
</jacoco:coverage>
167+
</target>
168+
161169
</project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors
3+
* This program and the accompanying materials are made available under
4+
* the terms of the Eclipse Public License 2.0 which is available at
5+
* http://www.eclipse.org/legal/epl-2.0
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Evgeny Mandrikov - initial API and implementation
11+
*
12+
*******************************************************************************/
13+
14+
package org.jacoco.ant;
15+
16+
import java.lang.reflect.Constructor;
17+
18+
public class IllegalReflectiveAccessTarget {
19+
20+
public static void main(String[] args) throws Exception {
21+
try {
22+
Class.forName("java.net.UnixDomainSocketAddress");
23+
} catch (ClassNotFoundException e) {
24+
// Java < 16
25+
return;
26+
}
27+
28+
final Constructor<?> c = Class.forName("java.lang.Module")
29+
.getDeclaredConstructors()[0];
30+
try {
31+
c.setAccessible(true);
32+
throw new AssertionError("Exception expected");
33+
} catch (RuntimeException e) {
34+
if (!e.getClass().getName()
35+
.equals("java.lang.reflect.InaccessibleObjectException")) {
36+
throw new AssertionError(
37+
"InaccessibleObjectException expected");
38+
}
39+
}
40+
}
41+
42+
}

0 commit comments

Comments
 (0)