Skip to content

Commit d508fa5

Browse files
committed
Support dynamic class loading and serialization
1.Dump dynamically defined classes to file system (specified by -agentlib:native-image-agent=dynmaic-class-dump-dir=) by Agent at a beforehand run and the dumped class files must be on build time's classpath to compile them into native-image. 2.Dynamically generated class's name could be decided at runtime(e.g. runtime serial number as postfix) or null when defineClass is invoked. The former is supported by using same rule to generate fixed names for both Agent runtime and native-image runtime. The latter is supported by retrieving class name from dumped class bytecode at native-image runtime. 3.Support JDK serialization/deserialization which replies on dynamic class loading and reflection. Known issue: 1.Don't support serialize proxied class, because the proxy class name differs at Agent runtime and native-image build time. 2.It is possible the jar file on classpath has a different signature file from dynamically generated class'. Current solution is to delete the signature file at native-image build time. 3.Warning message such as "WARNING: Method java.lang.Object.<clinit>() not found." will be reported at native-image build time. Because such method has been accessed via JNI calls at serialization time to calculate serializeVersionUID and the Agent has intercepted and recorded them.
1 parent 567c101 commit d508fa5

File tree

20 files changed

+1098
-44
lines changed

20 files changed

+1098
-44
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public static int onLoad(JNIJavaVM vm, CCharPointer options, @SuppressWarnings("
137137

138138
String traceOutputFile = null;
139139
String configOutputDir = null;
140+
String dynclassDumpDir = null;
140141
ConfigurationSet restrictConfigs = new ConfigurationSet();
141142
ConfigurationSet mergeConfigs = new ConfigurationSet();
142143
boolean restrict = false;
@@ -170,6 +171,13 @@ public static int onLoad(JNIJavaVM vm, CCharPointer options, @SuppressWarnings("
170171
if (token.startsWith("config-merge-dir=")) {
171172
mergeConfigs.addDirectory(Paths.get(configOutputDir));
172173
}
174+
} else if (token.startsWith("dynmaic-class-dump-dir=")) {
175+
if (dynclassDumpDir != null) {
176+
System.err.println(MESSAGE_PREFIX + "cannot specify dynmaic-class-dump-dir= more than once.");
177+
return 1;
178+
}
179+
dynclassDumpDir = getTokenValue(token);
180+
DynamicClassGenerationSupport.setDynClassDumpDir(dynclassDumpDir);
173181
} else if (token.startsWith("restrict-all-dir")) {
174182
/* Used for testing */
175183
restrictConfigs.addDirectory(Paths.get(getTokenValue(token)));

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

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@
3636
import static com.oracle.svm.agent.Support.getClassNameOrNull;
3737
import static com.oracle.svm.agent.Support.getDirectCallerClass;
3838
import static com.oracle.svm.agent.Support.getMethodDeclaringClass;
39+
import static com.oracle.svm.agent.Support.getMethodName;
3940
import static com.oracle.svm.agent.Support.getObjectArgument;
41+
import static com.oracle.svm.agent.Support.getIntArgument;
4042
import static com.oracle.svm.agent.Support.handles;
4143
import static com.oracle.svm.agent.Support.jniFunctions;
4244
import static com.oracle.svm.agent.Support.jvmtiEnv;
4345
import static com.oracle.svm.agent.Support.jvmtiFunctions;
4446
import static com.oracle.svm.agent.Support.testException;
4547
import static com.oracle.svm.agent.Support.toCString;
48+
import static com.oracle.svm.agent.Support.callObjectMethodL;
49+
4650
import static com.oracle.svm.agent.jvmti.JvmtiEvent.JVMTI_EVENT_BREAKPOINT;
4751
import static com.oracle.svm.agent.jvmti.JvmtiEvent.JVMTI_EVENT_CLASS_PREPARE;
4852
import static com.oracle.svm.agent.jvmti.JvmtiEvent.JVMTI_EVENT_NATIVE_METHOD_BIND;
@@ -62,7 +66,10 @@
6266
import java.util.concurrent.locks.ReentrantLock;
6367
import java.util.function.Supplier;
6468

69+
import com.oracle.svm.configure.trace.AccessAdvisor;
70+
import com.oracle.svm.core.jdk.serialize.MethodAccessorNameGenerator;
6571
import org.graalvm.compiler.core.common.NumUtil;
72+
import org.graalvm.compiler.phases.common.LazyValue;
6673
import org.graalvm.nativeimage.StackValue;
6774
import org.graalvm.nativeimage.UnmanagedMemory;
6875
import org.graalvm.nativeimage.c.function.CEntryPoint;
@@ -94,6 +101,7 @@
94101
import com.oracle.svm.configure.config.ConfigurationMethod;
95102
import com.oracle.svm.core.c.function.CEntryPointOptions;
96103
import com.oracle.svm.core.util.VMError;
104+
97105
import com.oracle.svm.jni.nativeapi.JNIEnvironment;
98106
import com.oracle.svm.jni.nativeapi.JNIMethodId;
99107
import com.oracle.svm.jni.nativeapi.JNINativeMethod;
@@ -129,6 +137,7 @@ final class BreakpointInterceptor {
129137
private static ProxyAccessVerifier proxyVerifier;
130138
private static ResourceAccessVerifier resourceVerifier;
131139

140+
private static AccessAdvisor accessAdvisor = new AccessAdvisor();
132141
private static Map<Long, Breakpoint> installedBreakpoints;
133142

134143
/**
@@ -160,7 +169,11 @@ final class BreakpointInterceptor {
160169

161170
private static final ThreadLocal<Boolean> recursive = ThreadLocal.withInitial(() -> Boolean.FALSE);
162171

163-
private static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass, JNIObjectHandle callerClass, String function, Object result, Object... args) {
172+
static {
173+
accessAdvisor.setInLivePhase(true);
174+
}
175+
176+
static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass, JNIObjectHandle callerClass, String function, Object result, Object... args) {
164177
if (traceWriter != null) {
165178
traceWriter.traceCall("reflect",
166179
function,
@@ -173,6 +186,18 @@ private static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, J
173186
}
174187
}
175188

189+
static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass,
190+
JNIObjectHandle callerClass, String function, boolean allowWrite, boolean unsafeAccess, Object result,
191+
String fieldName) {
192+
if (traceWriter != null) {
193+
traceWriter.traceCall("reflect", function, getClassNameOr(env, clazz, null, TraceWriter.UNKNOWN_VALUE),
194+
getClassNameOr(env, declaringClass, null, TraceWriter.UNKNOWN_VALUE),
195+
getClassNameOr(env, callerClass, null, TraceWriter.UNKNOWN_VALUE), result, allowWrite, unsafeAccess,
196+
fieldName);
197+
guarantee(!testException(env));
198+
}
199+
}
200+
176201
private static boolean forName(JNIEnvironment jni, Breakpoint bp) {
177202
JNIObjectHandle callerClass = getDirectCallerClass();
178203
JNIObjectHandle name = getObjectArgument(0);
@@ -504,6 +529,20 @@ private static boolean newInstance(JNIEnvironment jni, Breakpoint bp) {
504529
JNIMethodId result = nullPointer();
505530
String name = "<init>";
506531
String signature = "()V";
532+
/*
533+
* "sun.reflect.MethodAccessorGenerator$1" is added as Include in AccessAdvisor's
534+
* internalsFilter in order to support serialization/deserialization. But newInstance call
535+
* in sun.reflect.MethodAccessorGenerator$1 is invoked in 3 cases: 1. Reflection for method
536+
* invoke 2. Reflection for newInstance invoke 3. Serialization/deserialization The first
537+
* two cases are removed from native-image runtime. The third case is traced by
538+
* com.oracle.svm.agent.SerializationSupport.traceReflects. Therefore don't trace the call
539+
* here to avoid introducing unnecessary items in the reflection configuration file.
540+
*/
541+
String callerClassName = getClassNameOrNull(jni, callerClass);
542+
if (callerClassName != null && callerClassName.equals("sun.reflect.MethodAccessorGenerator$1")) {
543+
return false;
544+
}
545+
507546
JNIObjectHandle self = getObjectArgument(0);
508547
if (self.notEqual(nullHandle())) {
509548
try (CCharPointerHolder ctorName = toCString(name); CCharPointerHolder ctorSignature = toCString(signature)) {
@@ -675,6 +714,172 @@ private static boolean handleGetSystemResources(JNIEnvironment jni, Breakpoint b
675714
return allowed;
676715
}
677716

717+
/**
718+
* Replace the returned value of method sun.reflect.MethodAccessorGenerator.generateName(). The
719+
* returned generated name's postfix is changed from a counter value to the declaring class'
720+
* name. So the generated serialization constructor support class' name shall be fixed name from
721+
* different runs.
722+
*/
723+
@SuppressWarnings("unused")
724+
private static boolean generateName(JNIEnvironment jni, Breakpoint bp) {
725+
JNIObjectHandle callerClass = getDirectCallerClass();
726+
boolean isConstructor = getIntArgument(0) == 0 ? false : true;
727+
boolean forSerialization = getIntArgument(1) == 0 ? false : true;
728+
// Get declaringClass from generate() method
729+
String generatedClassName = getGeneratedClassName(jni, getObjectArgument(1, 1), isConstructor, forSerialization);
730+
try (CCharPointerHolder name = toCString(generatedClassName)) {
731+
JNIObjectHandle newRet = jniFunctions().getNewStringUTF().invoke(jni, name.get());
732+
if (jvmtiFunctions().ForceEarlyReturnObject().invoke(jvmtiEnv(), nullHandle(), newRet) == JvmtiError.JVMTI_ERROR_NONE) {
733+
return true;
734+
}
735+
}
736+
return false;
737+
}
738+
739+
private static String getGeneratedClassName(JNIEnvironment jni, JNIObjectHandle declaringClass, boolean isConstructor, boolean forSerialization) {
740+
String declaringClassName = getClassNameOrNull(jni, declaringClass);
741+
if (declaringClassName == null) {
742+
throw new RuntimeException("Cannot find class name");
743+
}
744+
return MethodAccessorNameGenerator.generateClassName(isConstructor, forSerialization, declaringClassName);
745+
}
746+
747+
/**
748+
* java.lang.ClassLoader.postDefineClass is always called in java.lang.ClassLoader.defineClass,
749+
* so intercepting postDefineClass is equivalent to intercepting defineClass but with extra
750+
* benefit of being always able to get defined class' name even if defineClass' classname
751+
* parameter is null.
752+
*/
753+
@SuppressWarnings("unused")
754+
private static boolean postDefineClass(JNIEnvironment jni, Breakpoint bp) {
755+
JNIObjectHandle callerClass = getDirectCallerClass();
756+
boolean isDynamicallyGenerated = false;
757+
// Get class name from the argument "name" of
758+
// defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
759+
// The first argument is implicitly "this", so "name" is the 2nd parameter.
760+
String nameFromDefineClassParam = fromJniString(jni, getObjectArgument(1, 1));
761+
final String definedClassName;
762+
// 1. Don't have a name for class before defining.
763+
// The class is dynamically generated.
764+
if (nameFromDefineClassParam == null) {
765+
isDynamicallyGenerated = true;
766+
definedClassName = getClassNameOrNull(jni, getObjectArgument(1));
767+
} else {
768+
definedClassName = nameFromDefineClassParam;
769+
// Filter out internal classes which are definitely not dynamically generated
770+
if (accessAdvisor.shouldIgnoreCaller(new LazyValue<>(() -> definedClassName))) {
771+
return false;
772+
}
773+
774+
// 2. Class with name starts with $ or contains $$ is usually dynamically generated
775+
String className = definedClassName.substring(definedClassName.lastIndexOf('.') + 1);
776+
if (className.startsWith("$") || className.contains("$$")) {
777+
isDynamicallyGenerated = true;
778+
} else {
779+
// 3. A dynamically defined class always return null
780+
// when call java.lang.ClassLoader.getResource(classname)
781+
// This is the accurate but slow way.
782+
JNIObjectHandle self = getObjectArgument(0);
783+
String asResourceName = definedClassName.replace('.', '/') + ".class";
784+
try (CCharPointerHolder resourceNameHolder = toCString(asResourceName);) {
785+
JNIObjectHandle resourceNameJString = jniFunctions().getNewStringUTF().invoke(jni, resourceNameHolder.get());
786+
JNIObjectHandle returnValue = callObjectMethodL(jni, self, handles().javaLangClassLoaderGetResource, resourceNameJString);
787+
isDynamicallyGenerated = returnValue.equal(nullHandle());
788+
}
789+
}
790+
}
791+
if (isDynamicallyGenerated) {
792+
DynamicClassGenerationSupport dynamicSupport = DynamicClassGenerationSupport.getDynamicClassGenerationSupport(jni, callerClass,
793+
definedClassName, traceWriter);
794+
if (!dynamicSupport.dumpDefinedClass()) {
795+
return false;
796+
}
797+
return dynamicSupport.traceReflects();
798+
} else {
799+
return true;
800+
}
801+
}
802+
803+
/**
804+
* Disable reflection inflation for 2 reasons:
805+
* <li>1. Javassit and Spring use reflection to call java.lang.ClassLoader.defineClass. A fixed
806+
* stacktrace would be easier to track to find out if the defineClass is called from reflection.
807+
* </li>
808+
* <li>2. native-image build time doesn't need any reflection inflation information so it's safe
809+
* to disable it.</li>
810+
*/
811+
@SuppressWarnings("unused")
812+
private static boolean inflationThreshold(JNIEnvironment jni, Breakpoint bp) {
813+
if (jvmtiFunctions().ForceEarlyReturnInt().invoke(jvmtiEnv(), nullHandle(), 10000) == JvmtiError.JVMTI_ERROR_NONE) {
814+
return true;
815+
} else {
816+
return false;
817+
}
818+
}
819+
820+
/**
821+
* Handle Class<?> sun.reflect.ClassDefiner.defineClass(String name, byte[] bytes, int off, int
822+
* len, ClassLoader parentClassLoader) Dump dynamic defined class to file.
823+
*/
824+
@SuppressWarnings("unused")
825+
private static boolean defineClass(JNIEnvironment jni, Breakpoint bp) {
826+
JNIObjectHandle callerClass = getDirectCallerClass();
827+
JNIMethodId method = getCallerMethod(4);
828+
String methodName = getMethodName(method);
829+
JNIObjectHandle declaringClass = getMethodDeclaringClass(method);
830+
String declaringClassName = getClassNameOr(jni, declaringClass, "null", "exception");
831+
832+
// Only dump dynamically defined class from sun.reflect.MethodAccessorGenerator.generate
833+
if (methodName.equals("generate") && declaringClassName != null &&
834+
declaringClassName.equals("sun.reflect.MethodAccessorGenerator")) {
835+
// isConstructor parameter of generate method
836+
boolean isConstructor = getIntArgument(4, 7) == 0 ? false : true;
837+
// forSerialization parameter of generate method
838+
boolean forSerialization = getIntArgument(4, 8) == 0 ? false : true;
839+
// The first method argument is class name
840+
String generatedClassName = fromJniString(jni, getObjectArgument(0));
841+
JNIObjectHandle class2Generate = getObjectArgument(4, 1);
842+
JNIObjectHandle serializationTargetClass = getObjectArgument(4, 9);
843+
DynamicClassGenerationSupport serializationSupport = DynamicClassGenerationSupport.getSerializeSupport(jni, callerClass,
844+
class2Generate, generatedClassName, serializationTargetClass, traceWriter);
845+
846+
if (isConstructor && !forSerialization) {
847+
int i = 0;
848+
// Walk along the stack trace to find out if current method is
849+
// called from serialization/deserialization
850+
while (true) {
851+
JNIMethodId m = getCallerMethod(i);
852+
if (m == nullPointer()) {
853+
break;
854+
}
855+
String cName = getClassNameOrNull(jni, getMethodDeclaringClass(m));
856+
String mName = getMethodName(m);
857+
if (cName == null || mName == null) {
858+
break;
859+
}
860+
String fullName = cName + "." + mName;
861+
// Mark is from serialization/deserialization
862+
if (fullName.equals("java.io.ObjectInputStream.readObject") || fullName.equals("java.io.ObjectOutputStream.writeObject")) {
863+
forSerialization = true;
864+
break;
865+
}
866+
i++;
867+
}
868+
}
869+
if (isConstructor && forSerialization) {
870+
if (!serializationSupport.dumpDefinedClass()) {
871+
return false;
872+
}
873+
if (serializationSupport.traceReflects()) {
874+
return true;
875+
}
876+
} else {
877+
return true;
878+
}
879+
}
880+
return false;
881+
}
882+
678883
private static boolean newProxyInstance(JNIEnvironment jni, Breakpoint bp) {
679884
JNIObjectHandle callerClass = getDirectCallerClass();
680885
JNIObjectHandle classLoader = getObjectArgument(0);
@@ -1212,6 +1417,13 @@ private interface BreakpointHandler {
12121417
brk("java/lang/reflect/Proxy", "getProxyClass", "(Ljava/lang/ClassLoader;[Ljava/lang/Class;)Ljava/lang/Class;", BreakpointInterceptor::getProxyClass),
12131418
brk("java/lang/reflect/Proxy", "newProxyInstance",
12141419
"(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;", BreakpointInterceptor::newProxyInstance),
1420+
/*
1421+
* For dumping dynamically generated classes
1422+
*/
1423+
brk("java/lang/ClassLoader", "postDefineClass", "(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V", BreakpointInterceptor::postDefineClass),
1424+
brk("sun/reflect/ClassDefiner", "defineClass", "(Ljava/lang/String;[BIILjava/lang/ClassLoader;)Ljava/lang/Class;", BreakpointInterceptor::defineClass),
1425+
brk("sun/reflect/MethodAccessorGenerator", "generateName", "(ZZ)Ljava/lang/String;", BreakpointInterceptor::generateName),
1426+
brk("sun/reflect/ReflectionFactory", "inflationThreshold", "()I", BreakpointInterceptor::inflationThreshold),
12151427

12161428
optionalBrk("java/util/ResourceBundle",
12171429
"getBundleImpl",

0 commit comments

Comments
 (0)