Skip to content

Commit

Permalink
Support dynamic class loading and serialization
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ziyilin committed Apr 8, 2020
1 parent ba46378 commit 955abc5
Show file tree
Hide file tree
Showing 20 changed files with 913 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public static int onLoad(JNIJavaVM vm, CCharPointer options, @SuppressWarnings("

String traceOutputFile = null;
String configOutputDir = null;
String dynclassDumpDir = null;
ConfigurationSet restrictConfigs = new ConfigurationSet();
ConfigurationSet mergeConfigs = new ConfigurationSet();
boolean restrict = false;
Expand Down Expand Up @@ -170,6 +171,13 @@ public static int onLoad(JNIJavaVM vm, CCharPointer options, @SuppressWarnings("
if (token.startsWith("config-merge-dir=")) {
mergeConfigs.addDirectory(Paths.get(configOutputDir));
}
} else if (token.startsWith("dynmaic-class-dump-dir=")) {
if (dynclassDumpDir != null) {
System.err.println(MESSAGE_PREFIX + "cannot specify dynmaic-class-dump-dir= more than once.");
return 1;
}
dynclassDumpDir = getTokenValue(token);
DynamicClassGenerationSupport.setDynClassDumpDir(dynclassDumpDir);
} else if (token.startsWith("restrict-all-dir")) {
/* Used for testing */
restrictConfigs.addDirectory(Paths.get(getTokenValue(token)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
import static com.oracle.svm.agent.Support.getClassNameOrNull;
import static com.oracle.svm.agent.Support.getDirectCallerClass;
import static com.oracle.svm.agent.Support.getMethodDeclaringClass;
import static com.oracle.svm.agent.Support.getMethodName;
import static com.oracle.svm.agent.Support.getObjectArgument;
import static com.oracle.svm.agent.Support.getIntArgument;
import static com.oracle.svm.agent.Support.handles;
import static com.oracle.svm.agent.Support.jniFunctions;
import static com.oracle.svm.agent.Support.jvmtiEnv;
Expand All @@ -62,6 +64,7 @@
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

import com.oracle.svm.core.jdk.serialize.MethodAccessorNameGenerator;
import org.graalvm.compiler.core.common.NumUtil;
import org.graalvm.nativeimage.StackValue;
import org.graalvm.nativeimage.UnmanagedMemory;
Expand Down Expand Up @@ -160,7 +163,7 @@ final class BreakpointInterceptor {

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

private static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass, JNIObjectHandle callerClass, String function, Object result, Object... args) {
static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass, JNIObjectHandle callerClass, String function, Object result, Object... args) {
if (traceWriter != null) {
traceWriter.traceCall("reflect",
function,
Expand All @@ -173,6 +176,18 @@ private static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, J
}
}

static void traceBreakpoint(JNIEnvironment env, JNIObjectHandle clazz, JNIObjectHandle declaringClass,
JNIObjectHandle callerClass, String function, boolean allowWrite, boolean unsafeAccess, Object result,
String fieldName) {
if (traceWriter != null) {
traceWriter.traceCall("reflect", function, getClassNameOr(env, clazz, null, TraceWriter.UNKNOWN_VALUE),
getClassNameOr(env, declaringClass, null, TraceWriter.UNKNOWN_VALUE),
getClassNameOr(env, callerClass, null, TraceWriter.UNKNOWN_VALUE), result, allowWrite, unsafeAccess,
fieldName);
guarantee(!testException(env));
}
}

private static boolean forName(JNIEnvironment jni, Breakpoint bp) {
JNIObjectHandle callerClass = getDirectCallerClass();
JNIObjectHandle name = getObjectArgument(0);
Expand Down Expand Up @@ -504,6 +519,20 @@ private static boolean newInstance(JNIEnvironment jni, Breakpoint bp) {
JNIMethodId result = nullPointer();
String name = "<init>";
String signature = "()V";
/*
* "sun.reflect.MethodAccessorGenerator$1" is added as Include in AccessAdvisor's
* internalsFilter in order to support serialization/deserialization. But newInstance call
* in sun.reflect.MethodAccessorGenerator$1 is invoked in 3 cases: 1. Reflection for method
* invoke 2. Reflection for newInstance invoke 3. Serialization/deserialization The first
* two cases are removed from native-image runtime. The third case is traced by
* com.oracle.svm.agent.SerializationSupport.traceReflects. Therefore don't trace the call
* here to avoid introducing unnecessary items in the reflection configuration file.
*/
String callerClassName = getClassNameOrNull(jni, callerClass);
if (callerClassName != null && callerClassName.equals("sun.reflect.MethodAccessorGenerator$1")) {
return false;
}

JNIObjectHandle self = getObjectArgument(0);
if (self.notEqual(nullHandle())) {
try (CCharPointerHolder ctorName = toCString(name); CCharPointerHolder ctorSignature = toCString(signature)) {
Expand Down Expand Up @@ -675,6 +704,166 @@ private static boolean handleGetSystemResources(JNIEnvironment jni, Breakpoint b
return allowed;
}

/**
* Replace the returned value of method sun.reflect.MethodAccessorGenerator.generateName(). The
* returned generated name's postfix is changed from a counter value to the declaring class'
* name. So the generated serialization constructor support class' name shall be fixed name from
* different runs.
*/
@SuppressWarnings("unused")
private static boolean generateName(JNIEnvironment jni, Breakpoint bp) {
JNIObjectHandle callerClass = getDirectCallerClass();
boolean isConstructor = getIntArgument(0) == 0 ? false : true;
boolean forSerialization = getIntArgument(1) == 0 ? false : true;
// Get declaringClass from generate() method
String generatedClassName = getGeneratedClassName(jni, getObjectArgument(1, 1), isConstructor, forSerialization);
try (CCharPointerHolder name = toCString(generatedClassName)) {
JNIObjectHandle newRet = jniFunctions().getNewStringUTF().invoke(jni, name.get());
if (jvmtiFunctions().ForceEarlyReturnObject().invoke(jvmtiEnv(), nullHandle(), newRet) == JvmtiError.JVMTI_ERROR_NONE) {
return true;
}
}
return false;
}

private static String getGeneratedClassName(JNIEnvironment jni, JNIObjectHandle declaringClass, boolean isConstructor, boolean forSerialization) {
String declaringClassName = getClassNameOrNull(jni, declaringClass);
if (declaringClassName == null) {
throw new RuntimeException("Cannot find class name");
}
return MethodAccessorNameGenerator.generateClassName(isConstructor, forSerialization, declaringClassName);
}

/**
* java.lang.ClassLoader.postDefineClass is always called in java.lang.ClassLoader.defineClass,
* so intercepting postDefineClass is equivalent to intercepting defineClass but with extra
* benefit of being able to get defined class' name even if the given classname parameter of
* defineClass' is null.
*/
@SuppressWarnings("unused")
private static boolean postDefineClass(JNIEnvironment jni, Breakpoint bp) {
JNIObjectHandle callerClass = getDirectCallerClass();
boolean isDynamicallyGenerated = false;
// Get class name from the argument "name" of
// defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
// The first argument is implicitly "this", so "name" is the 2nd.
String definedClassName = fromJniString(jni, getObjectArgument(1, 1));
if (definedClassName == null) {
// Don't have a name for class before defining.
// The class is dynamically generated
isDynamicallyGenerated = true;
definedClassName = getClassNameOrNull(jni, getObjectArgument(1));
} else {
String className = definedClassName.substring(definedClassName.lastIndexOf('.') + 1);
// Class name starts with $ or contain $$ is usually dynamically generated
if (className.startsWith("$") || className.contains("$$")) {
isDynamicallyGenerated = true;
} else {
/**
* When the defineClass is called via reflection, it's usually the dynamic class
* loading. Here we check the 4th frame. If it's
* sun.reflect.NativeMethodAccessorImpl.invoke or
* sun.reflect.NativeMethodAccessorImpl.invoke0, then it's a reflection call.
*/
JNIMethodId invoke0MethodId = getCallerMethod(3);
String invoke0MethodName = getMethodName(invoke0MethodId);
String invokeClassName = getClassNameOrNull(jni, getMethodDeclaringClass(invoke0MethodId));
if (invokeClassName != null && invokeClassName.equals("sun.reflect.NativeMethodAccessorImpl") && invoke0MethodName.startsWith("invoke")) {
isDynamicallyGenerated = true;
}
}
}
if (isDynamicallyGenerated) {
DynamicClassGenerationSupport dynamicSupport = DynamicClassGenerationSupport.getDynamicClassGenerationSupport(jni, callerClass,
definedClassName, traceWriter);
if (!dynamicSupport.dumpDefinedClass()) {
return false;
}
return dynamicSupport.traceReflects();
} else {
return true;
}
}

/*
* Disable reflection inflation for 2 reasons: 1. Javassit and Spring use reflection to call
* java.lang.ClassLoader.defineClass. A fixed stacktrace would be easier to track to find out if
* the defineClass is called from reflection. 2. native-image build time doesn't need any
* reflection inflation information so it's safe to disable it.
*/
@SuppressWarnings("unused")
private static boolean inflationThreshold(JNIEnvironment jni, Breakpoint bp) {
if (jvmtiFunctions().ForceEarlyReturnInt().invoke(jvmtiEnv(), nullHandle(), 10000) == JvmtiError.JVMTI_ERROR_NONE) {
return true;
} else {
return false;
}
}

/**
* Handle Class<?> sun.reflect.ClassDefiner.defineClass(String name, byte[] bytes, int off, int
* len, ClassLoader parentClassLoader) Dump dynamic defined class to file.
*
*/
@SuppressWarnings("unused")
private static boolean defineClass(JNIEnvironment jni, Breakpoint bp) {
JNIObjectHandle callerClass = getDirectCallerClass();
JNIMethodId method = getCallerMethod(4);
String methodName = getMethodName(method);
JNIObjectHandle declaringClass = getMethodDeclaringClass(method);
String declaringClassName = getClassNameOr(jni, declaringClass, "null", "exception");

// Only dump dynamically defined class from sun.reflect.MethodAccessorGenerator.generate
if (methodName.equals("generate") && declaringClassName != null &&
declaringClassName.equals("sun.reflect.MethodAccessorGenerator")) {
// isConstructor parameter of generate method
boolean isConstructor = getIntArgument(4, 7) == 0 ? false : true;
// forSerialization parameter of generate method
boolean forSerialization = getIntArgument(4, 8) == 0 ? false : true;
// The first method argument is class name
String generatedClassName = fromJniString(jni, getObjectArgument(0));
JNIObjectHandle class2Generate = getObjectArgument(4, 1);
JNIObjectHandle serializationTargetClass = getObjectArgument(4, 9);
DynamicClassGenerationSupport serializationSupport = DynamicClassGenerationSupport.getSerializeSupport(jni, callerClass,
class2Generate, generatedClassName, serializationTargetClass, traceWriter);

if (isConstructor && !forSerialization) {
int i = 0;
// Walk along the stack trace to find out if current method is
// called from serialization/deserialization
while (true) {
JNIMethodId m = getCallerMethod(i);
if (m == nullPointer()) {
break;
}
String cName = getClassNameOrNull(jni, getMethodDeclaringClass(m));
String mName = getMethodName(m);
if (cName == null || mName == null) {
break;
}
String fullName = cName + "." + mName;
// Mark is from serialization/deserialization
if (fullName.equals("java.io.ObjectInputStream.readObject") || fullName.equals("java.io.ObjectOutputStream.writeObject")) {
forSerialization = true;
break;
}
i++;
}
}
if (isConstructor && forSerialization) {
if (!serializationSupport.dumpDefinedClass()) {
return false;
}
if (serializationSupport.traceReflects()) {
return true;
}
} else {
return true;
}
}
return false;
}

private static boolean newProxyInstance(JNIEnvironment jni, Breakpoint bp) {
JNIObjectHandle callerClass = getDirectCallerClass();
JNIObjectHandle classLoader = getObjectArgument(0);
Expand Down Expand Up @@ -1212,6 +1401,13 @@ private interface BreakpointHandler {
brk("java/lang/reflect/Proxy", "getProxyClass", "(Ljava/lang/ClassLoader;[Ljava/lang/Class;)Ljava/lang/Class;", BreakpointInterceptor::getProxyClass),
brk("java/lang/reflect/Proxy", "newProxyInstance",
"(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;", BreakpointInterceptor::newProxyInstance),
/*
* For dumping dynamically generated classes
*/
brk("java/lang/ClassLoader", "postDefineClass", "(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V", BreakpointInterceptor::postDefineClass),
brk("sun/reflect/ClassDefiner", "defineClass", "(Ljava/lang/String;[BIILjava/lang/ClassLoader;)Ljava/lang/Class;", BreakpointInterceptor::defineClass),
brk("sun/reflect/MethodAccessorGenerator", "generateName", "(ZZ)Ljava/lang/String;", BreakpointInterceptor::generateName),
brk("sun/reflect/ReflectionFactory", "inflationThreshold", "()I", BreakpointInterceptor::inflationThreshold),

optionalBrk("java/util/ResourceBundle",
"getBundleImpl",
Expand Down
Loading

0 comments on commit 955abc5

Please sign in to comment.