Skip to content

Commit 98f8014

Browse files
committed
Support dynamic class loading
Support dynamic class loading for native image
1 parent e734181 commit 98f8014

File tree

28 files changed

+1243
-44
lines changed

28 files changed

+1243
-44
lines changed

substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static com.oracle.svm.core.util.VMError.guarantee;
2828
import static com.oracle.svm.jni.JNIObjectHandles.nullHandle;
2929
import static com.oracle.svm.jvmtiagentbase.Support.callObjectMethod;
30+
import static com.oracle.svm.jvmtiagentbase.Support.callObjectMethodL;
3031
import static com.oracle.svm.jvmtiagentbase.Support.check;
3132
import static com.oracle.svm.jvmtiagentbase.Support.checkJni;
3233
import static com.oracle.svm.jvmtiagentbase.Support.checkNoException;
@@ -39,6 +40,7 @@
3940
import static com.oracle.svm.jvmtiagentbase.Support.getClassNameOrNull;
4041
import static com.oracle.svm.jvmtiagentbase.Support.getDirectCallerClass;
4142
import static com.oracle.svm.jvmtiagentbase.Support.getMethodDeclaringClass;
43+
import static com.oracle.svm.jvmtiagentbase.Support.getMethodFullNameAtFrame;
4244
import static com.oracle.svm.jvmtiagentbase.Support.getObjectArgument;
4345
import static com.oracle.svm.jvmtiagentbase.Support.handleException;
4446
import static com.oracle.svm.jvmtiagentbase.Support.jniFunctions;
@@ -52,18 +54,22 @@
5254
import static com.oracle.svm.jvmtiagentbase.jvmti.JvmtiEvent.JVMTI_EVENT_NATIVE_METHOD_BIND;
5355
import static org.graalvm.word.WordFactory.nullPointer;
5456

57+
import java.io.IOException;
5558
import java.nio.ByteBuffer;
5659
import java.nio.ByteOrder;
60+
import java.security.NoSuchAlgorithmException;
5761
import java.util.ArrayList;
5862
import java.util.Arrays;
5963
import java.util.HashMap;
6064
import java.util.List;
6165
import java.util.Map;
66+
import java.util.Set;
6267
import java.util.concurrent.ConcurrentHashMap;
6368
import java.util.concurrent.ConcurrentMap;
6469
import java.util.concurrent.locks.ReentrantLock;
6570

6671
import org.graalvm.compiler.core.common.NumUtil;
72+
import org.graalvm.compiler.phases.common.LazyValue;
6773
import org.graalvm.nativeimage.StackValue;
6874
import org.graalvm.nativeimage.UnmanagedMemory;
6975
import org.graalvm.nativeimage.c.function.CEntryPoint;
@@ -80,6 +86,7 @@
8086
import org.graalvm.nativeimage.c.type.WordPointer;
8187
import org.graalvm.word.WordFactory;
8288

89+
import com.oracle.svm.configure.trace.AccessAdvisor;
8390
import com.oracle.svm.core.c.function.CEntryPointOptions;
8491
import com.oracle.svm.core.util.VMError;
8592
import com.oracle.svm.jni.JNIObjectHandles;
@@ -126,7 +133,8 @@ final class BreakpointInterceptor {
126133
private static NativeImageAgent agent;
127134

128135
private static Map<Long, Breakpoint> installedBreakpoints;
129-
136+
private static Set<String> definedClasses = ConcurrentHashMap.newKeySet();
137+
private static List<String> unsupportedExceptions = new ArrayList<>();
130138
/**
131139
* A map from {@link JNIMethodId} to entry point addresses for bound Java {@code native}
132140
* methods, NOT considering our intercepting functions, i.e., these are the original entry
@@ -169,6 +177,18 @@ private static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, J
169177
}
170178
}
171179

180+
static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass,
181+
JNIObjectHandle callerClass, String function, boolean allowWrite, boolean unsafeAccess, Object result,
182+
String fieldName) {
183+
if (traceWriter != null) {
184+
traceWriter.traceCall("reflect", function, getClassNameOr(env, clazz, null, TraceWriter.UNKNOWN_VALUE),
185+
getClassNameOr(env, declaringClass, null, TraceWriter.UNKNOWN_VALUE),
186+
getClassNameOr(env, callerClass, null, TraceWriter.UNKNOWN_VALUE), result, allowWrite, unsafeAccess,
187+
fieldName);
188+
guarantee(!testException(env));
189+
}
190+
}
191+
172192
private static boolean forName(JNIEnvironment jni, Breakpoint bp) {
173193
JNIObjectHandle callerClass = getDirectCallerClass();
174194
JNIObjectHandle name = getObjectArgument(0);
@@ -573,6 +593,96 @@ private static boolean handleGetSystemResources(JNIEnvironment jni, Breakpoint b
573593
return true;
574594
}
575595

596+
/**
597+
* java.lang.ClassLoader.postDefineClass is always called in java.lang.ClassLoader.defineClass,
598+
* so intercepting postDefineClass is equivalent to intercepting defineClass but with extra
599+
* benefit of being always able to get defined class' name even if defineClass' classname
600+
* parameter is null.
601+
*/
602+
@SuppressWarnings("unused")
603+
private static boolean postDefineClass(JNIEnvironment jni, Breakpoint bp) {
604+
boolean isDynamicallyGenerated = false;
605+
606+
// Get class name from the argument "name" of
607+
// defineClass(String name, byte[] b, int off, int len)
608+
// The first argument is implicitly "this", so "name" is the 2nd parameter.
609+
String nameFromDefineClassParam = fromJniString(jni, getObjectArgument(1, 1));
610+
final String definedClassName;
611+
JNIObjectHandle self = getObjectArgument(0);
612+
// 1. Don't have a name for class before defining.
613+
// The class is dynamically generated.
614+
if (nameFromDefineClassParam == null) {
615+
isDynamicallyGenerated = true;
616+
// Get name from parameter "c" of method postDefineClass(Class<?> c, ProtectionDomain
617+
// pd)
618+
definedClassName = getClassNameOrNull(jni, getObjectArgument(1));
619+
} else {
620+
definedClassName = nameFromDefineClassParam;
621+
// Filter out internal classes which are definitely not dynamically generated
622+
// CallerClass is always java.lang.ClassLoader, we only check the defined class
623+
AccessAdvisor postDefineCLassAccessAdvisor = new AccessAdvisor();
624+
postDefineCLassAccessAdvisor.setInLivePhase(true);
625+
if (postDefineCLassAccessAdvisor.shouldIgnore(new LazyValue<>(() -> definedClassName), new LazyValue<>(() -> null))) {
626+
isDynamicallyGenerated = false;
627+
}
628+
629+
// 2. Class with name starts with $ or contains $$ is usually dynamically generated
630+
String className = definedClassName.substring(definedClassName.lastIndexOf('.') + 1);
631+
if (className.startsWith("$") || className.contains("$$")) {
632+
isDynamicallyGenerated = true;
633+
} else {
634+
// 3. A dynamically defined class always return null
635+
// when call java.lang.ClassLoader.getResource(classname)
636+
// This is the accurate but slow way.
637+
String asResourceName = definedClassName.replace('.', '/') + ".class";
638+
try (CCharPointerHolder resourceNameHolder = toCString(asResourceName);) {
639+
JNIObjectHandle resourceNameJString = jniFunctions().getNewStringUTF().invoke(jni, resourceNameHolder.get());
640+
JNIObjectHandle returnValue = callObjectMethodL(jni, self, agent.handles().javaLangClassLoaderGetResource, resourceNameJString);
641+
isDynamicallyGenerated = returnValue.equal(nullHandle());
642+
}
643+
}
644+
}
645+
646+
// CallerClass is always java.lang.ClassLoader, we only check the defined class
647+
Object result = false;
648+
boolean justAdded = definedClasses.add(definedClassName);
649+
if (isDynamicallyGenerated) {
650+
if (!justAdded) {
651+
unsupportedExceptions.add("Class " + definedClassName + " has been defined before. Multiple definitions are not supported.\n" +
652+
ClassLoaderDefineClassSupport.getStackTrace(jni).toString());
653+
return true;
654+
}
655+
// Check the caller is using byte array or directedBuffer
656+
String caller = getMethodFullNameAtFrame(jni, 1);
657+
boolean isByteArray = "java.lang.ClassLoader.defineClass(Ljava/lang/String;[BIILjava/security/ProtectionDomain;)Ljava/lang/Class;".equals(caller);
658+
// Verify if defineClass succeeds
659+
// As we hook on postDefineClass method which is the last step of defineClass, so if
660+
// it can execute successfully, the whole defineClass method can execute
661+
// successfully
662+
JNIValue args = StackValue.get(2, JNIValue.class);
663+
args.addressOf(0).setObject(getObjectArgument(1));
664+
args.addressOf(1).setObject(getObjectArgument(2));
665+
jniFunctions().getCallVoidMethodA().invoke(jni, self, bp.method, args);
666+
if (clearException(jni)) {
667+
// No need to proceed if any exception happens
668+
result = false;
669+
} else {
670+
result = true;
671+
}
672+
try {
673+
JNIObjectHandle callerClass = getDirectCallerClass();
674+
ClassLoaderDefineClassSupport dynamicSupport = new ClassLoaderDefineClassSupport(jni, callerClass,
675+
definedClassName, traceWriter, agent, isByteArray);
676+
dynamicSupport.trace(result);
677+
return true;
678+
} catch (NoSuchAlgorithmException e) {
679+
throw new RuntimeException(e);
680+
}
681+
} else {
682+
return true;
683+
}
684+
}
685+
576686
private static boolean newProxyInstance(JNIEnvironment jni, Breakpoint bp) {
577687
JNIObjectHandle callerClass = getDirectCallerClass();
578688
JNIObjectHandle classLoader = getObjectArgument(0);
@@ -1269,15 +1379,29 @@ private static void bindNativeBreakpoint(JNIEnvironment jni, NativeBreakpoint bp
12691379
}
12701380
}
12711381

1382+
public static void reportExceptions() {
1383+
if (!unsupportedExceptions.isEmpty()) {
1384+
System.err.println(unsupportedExceptions.size() + " unsupported features are detected ");
1385+
StringBuilder errorMsg = new StringBuilder();
1386+
for (int i = 0; i < unsupportedExceptions.size(); i++) {
1387+
errorMsg.append(unsupportedExceptions.get(i)).append("\n");
1388+
}
1389+
throw new UnsupportedOperationException(errorMsg.toString());
1390+
} else {
1391+
unsupportedExceptions = null;
1392+
}
1393+
}
1394+
12721395
public static void onUnload() {
12731396
installedBreakpoints = null;
12741397
nativeBreakpoints = null;
12751398
observedExplicitLoadClassCallSites = null;
12761399
traceWriter = null;
1400+
definedClasses = null;
12771401
}
12781402

12791403
private interface BreakpointHandler {
1280-
boolean dispatch(JNIEnvironment jni, Breakpoint bp);
1404+
boolean dispatch(JNIEnvironment jni, Breakpoint bp) throws IOException;
12811405
}
12821406

12831407
private static final BreakpointSpecification[] BREAKPOINT_SPECIFICATIONS = {
@@ -1319,6 +1443,10 @@ private interface BreakpointHandler {
13191443
brk("java/lang/reflect/Proxy", "getProxyClass", "(Ljava/lang/ClassLoader;[Ljava/lang/Class;)Ljava/lang/Class;", BreakpointInterceptor::getProxyClass),
13201444
brk("java/lang/reflect/Proxy", "newProxyInstance",
13211445
"(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;", BreakpointInterceptor::newProxyInstance),
1446+
/*
1447+
* For dumping dynamically generated classes
1448+
*/
1449+
brk("java/lang/ClassLoader", "postDefineClass", "(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V", BreakpointInterceptor::postDefineClass),
13221450

13231451
brk("java/io/ObjectStreamClass", "<init>", "(Ljava/lang/Class;)V", BreakpointInterceptor::objectStreamClassConstructor),
13241452
optionalBrk("java/util/ResourceBundle",
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
3+
* Copyright (c) 2020, 2021, Alibaba Group Holding Limited. All rights reserved.
4+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5+
*
6+
* This code is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License version 2 only, as
8+
* published by the Free Software Foundation. Oracle designates this
9+
* particular file as subject to the "Classpath" exception as provided
10+
* by Oracle in the LICENSE file that accompanied this code.
11+
*
12+
* This code is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15+
* version 2 for more details (a copy is included in the LICENSE file that
16+
* accompanied this code).
17+
*
18+
* You should have received a copy of the GNU General Public License version
19+
* 2 along with this work; if not, write to the Free Software Foundation,
20+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21+
*
22+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23+
* or visit www.oracle.com if you need additional information or have any
24+
* questions.
25+
*/
26+
package com.oracle.svm.agent;
27+
28+
import com.oracle.svm.core.util.JavaClassUtil;
29+
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
30+
import com.oracle.svm.jni.nativeapi.JNIMethodId;
31+
import com.oracle.svm.jni.nativeapi.JNIObjectHandle;
32+
import org.graalvm.nativeimage.c.type.CCharPointer;
33+
import org.graalvm.nativeimage.c.type.CTypeConversion;
34+
import org.graalvm.nativeimage.c.type.VoidPointer;
35+
import org.graalvm.word.WordFactory;
36+
37+
import java.nio.ByteBuffer;
38+
import java.security.NoSuchAlgorithmException;
39+
40+
import static com.oracle.svm.jvmtiagentbase.Support.getIntArgument;
41+
import static com.oracle.svm.jvmtiagentbase.Support.getMethodFullNameAtFrame;
42+
import static com.oracle.svm.jvmtiagentbase.Support.getObjectArgument;
43+
import static com.oracle.svm.jvmtiagentbase.Support.jniFunctions;
44+
45+
/**
46+
* Support dynamic class loading that is implemented by java.lang.ClassLoader.defineClass.
47+
*/
48+
public class ClassLoaderDefineClassSupport {
49+
50+
private static final String DEFINE_CLASS_BYTEARRAY_SIG = "java.lang.ClassLoader.defineClass(Ljava/lang/String;[BII)Ljava/lang/Class;";
51+
protected JNIEnvironment jni;
52+
protected JNIObjectHandle callerClass;
53+
protected final String generatedClassName;
54+
protected TraceWriter traceWriter;
55+
protected NativeImageAgent agent;
56+
protected String generatedClassHashCode = null;
57+
protected byte[] values = null;
58+
private boolean isByteArray;
59+
60+
protected String callerMethod;
61+
62+
public ClassLoaderDefineClassSupport(JNIEnvironment jni, JNIObjectHandle callerClass, String generatedClassName, TraceWriter traceWriter, NativeImageAgent agent, boolean isByteArray) {
63+
this.jni = jni;
64+
this.callerClass = callerClass;
65+
// Make sure use qualified name for generatedClassName
66+
this.generatedClassName = generatedClassName.replace('/', '.');
67+
this.traceWriter = traceWriter;
68+
this.agent = agent;
69+
callerMethod = getMethodFullNameAtFrame(jni, 1);
70+
this.isByteArray = isByteArray;
71+
}
72+
73+
protected byte[] getClassContentsFromByteArray() {
74+
// bytes parameter of defineClass method
75+
JNIObjectHandle bytes = getClassDefinition();
76+
// len parameter of defineClass method
77+
int length = getClassDefinitionBytesLength();
78+
// Get generated class' byte array
79+
CCharPointer byteArray = jniFunctions().getGetByteArrayElements().invoke(jni, bytes, WordFactory.nullPointer());
80+
byte[] contents = new byte[length];
81+
try {
82+
CTypeConversion.asByteBuffer(byteArray, length).get(contents);
83+
} finally {
84+
jniFunctions().getReleaseByteArrayElements().invoke(jni, bytes, byteArray, 0);
85+
}
86+
return contents;
87+
}
88+
89+
protected byte[] getClassContentsFromDirectBuffer() {
90+
// DirectBuffer parameter of defineClass
91+
JNIObjectHandle directbuffer = getClassDefinition();
92+
93+
// Get byte array from DirectBuffer
94+
VoidPointer baseAddr = jniFunctions().getGetDirectBufferAddress().invoke(jni, directbuffer);
95+
JNIMethodId limitMId = agent.handles().getMethodId(jni, agent.handles().javaNioByteBuffer, "limit", "()I", false);
96+
int limit = jniFunctions().getCallIntMethod().invoke(jni, directbuffer, limitMId);
97+
ByteBuffer classContentsAsByteBuffer = CTypeConversion.asByteBuffer(baseAddr, limit);
98+
byte[] contents = new byte[classContentsAsByteBuffer.limit()];
99+
classContentsAsByteBuffer.get(contents);
100+
classContentsAsByteBuffer.position(0);
101+
return contents;
102+
}
103+
104+
private String calculateGeneratedClassSHA() throws NoSuchAlgorithmException {
105+
if (generatedClassHashCode == null) {
106+
generatedClassHashCode = JavaClassUtil.getSHAWithoutSourceFileInfo(getClassContents());
107+
}
108+
return generatedClassHashCode;
109+
}
110+
111+
public static StringBuilder getStackTrace(JNIEnvironment jni) {
112+
StringBuilder trace = new StringBuilder();
113+
int i = 0;
114+
int maxDepth = 20;
115+
while (i < maxDepth) {
116+
String methodName = getMethodFullNameAtFrame(jni, i++);
117+
if (methodName == null) {
118+
break;
119+
}
120+
trace.append(" ").append(methodName).append("\n");
121+
}
122+
if (i >= maxDepth) {
123+
trace.append(" ").append("...").append("\n");
124+
}
125+
return trace;
126+
}
127+
128+
public boolean checkSupported() {
129+
return DEFINE_CLASS_BYTEARRAY_SIG.equals(callerMethod);
130+
}
131+
132+
public void trace(Object result) throws NoSuchAlgorithmException {
133+
if (values == null) {
134+
values = getClassContents();
135+
}
136+
generatedClassHashCode = calculateGeneratedClassSHA();
137+
if (generatedClassName != null && values != null && result != null) {
138+
// Trace dynamically generated class in config file
139+
traceWriter.traceDefineClass(generatedClassName, generatedClassHashCode, values, result);
140+
}
141+
}
142+
143+
public byte[] getClassContents() {
144+
assert checkSupported();
145+
if (values == null) {
146+
if (isByteArray) {
147+
values = getClassContentsFromByteArray();
148+
} else {
149+
values = getClassContentsFromDirectBuffer();
150+
}
151+
152+
}
153+
return values;
154+
}
155+
156+
/**
157+
* Get value of argument "b" from java.lang.ClassLoader.defineClass(String name, byte[] b, int
158+
* off, int len, ProtectionDomain protectionDomain) "b" is the 3rd argument. because the 1st
159+
* argument of instance method is always "this"
160+
*/
161+
protected JNIObjectHandle getClassDefinition() {
162+
assert checkSupported();
163+
return getObjectArgument(1, 2);
164+
}
165+
166+
/**
167+
* Get value of argument "len" from java.lang.ClassLoader. defineClass(String name, byte[] b,
168+
* int off, int len, ProtectionDomain protectionDomain) "len" is the 5th argument. because the
169+
* 1st argument of instance method is always "this"
170+
*/
171+
protected int getClassDefinitionBytesLength() {
172+
assert checkSupported();
173+
return getIntArgument(1, 4);
174+
}
175+
}

0 commit comments

Comments
 (0)