mirror of
https://github.com/corda/corda.git
synced 2025-02-25 11:03:01 +00:00
ENT-1906: Allow DJVM code to throw and catch sandbox exceptions. (#4088)
* First phase of supporting exceptions within the DJVM. * Suppress unwanted inspection warnings about Kotlin/Java Map. * Add support for exception stack traces within the sandbox. * Simple review fixes. * Extra fixes after review. * Add DJVM support for String.intern(). * Partially restore implementation of SandboxClassLoader.loadClass(). * More review fixes.
This commit is contained in:
parent
e62a3edcd1
commit
e10119031c
@ -33,7 +33,6 @@ dependencies {
|
||||
|
||||
// ASM: byte code manipulation library
|
||||
compile "org.ow2.asm:asm:$asm_version"
|
||||
compile "org.ow2.asm:asm-tree:$asm_version"
|
||||
compile "org.ow2.asm:asm-commons:$asm_version"
|
||||
|
||||
// ClassGraph: classpath scanning
|
||||
@ -62,6 +61,7 @@ shadowJar {
|
||||
exclude 'sandbox/java/lang/Comparable.class'
|
||||
exclude 'sandbox/java/lang/Enum.class'
|
||||
exclude 'sandbox/java/lang/Iterable.class'
|
||||
exclude 'sandbox/java/lang/StackTraceElement.class'
|
||||
exclude 'sandbox/java/lang/StringBuffer.class'
|
||||
exclude 'sandbox/java/lang/StringBuilder.class'
|
||||
exclude 'sandbox/java/nio/**'
|
||||
|
@ -3,10 +3,10 @@ package sandbox.java.lang;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.lang.Appendable]
|
||||
* to keep [sandbox.java.lang.StringBuilder], [sandbox.java.lang.StringBuffer]
|
||||
* and [sandbox.java.lang.String] honest.
|
||||
* Note that it does not extend [java.lang.Appendable].
|
||||
* This is a dummy class that implements just enough of {@link java.lang.Appendable}
|
||||
* to keep {@link sandbox.java.lang.StringBuilder}, {@link sandbox.java.lang.StringBuffer}
|
||||
* and {@link sandbox.java.lang.String} honest.
|
||||
* Note that it does not extend {@link java.lang.Appendable}.
|
||||
*/
|
||||
public interface Appendable {
|
||||
|
||||
|
@ -3,8 +3,8 @@ package sandbox.java.lang;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.lang.CharSequence]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.lang.CharSequence}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
public interface CharSequence extends java.lang.CharSequence {
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
package sandbox.java.lang;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.lang.Comparable]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.lang.Comparable}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
public interface Comparable<T> extends java.lang.Comparable<T> {
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
package sandbox.java.lang;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Pinned exceptions inherit from {@link java.lang.Throwable}, but we
|
||||
* still need to be able to pass them through the sandbox's
|
||||
* exception handlers. In which case we will wrap them inside
|
||||
* one of these.
|
||||
*
|
||||
* Exceptions wrapped inside one of these cannot be caught.
|
||||
*
|
||||
* Also used for passing exceptions through finally blocks without
|
||||
* any expensive unwrapping to {@link sandbox.java.lang.Throwable}
|
||||
* based types.
|
||||
*/
|
||||
final class DJVMThrowableWrapper extends Throwable {
|
||||
private final java.lang.Throwable throwable;
|
||||
|
||||
DJVMThrowableWrapper(java.lang.Throwable t) {
|
||||
throwable = t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent this wrapper from creating its own stack trace.
|
||||
*/
|
||||
@Override
|
||||
public final Throwable fillInStackTrace() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
final java.lang.Throwable fromDJVM() {
|
||||
return throwable;
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package sandbox.java.lang;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* This is a dummy class. We will load the actual Enum class at run-time.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
public abstract class Enum<E extends Enum<E>> extends Object implements Comparable<E>, Serializable {
|
||||
|
||||
private final String name;
|
||||
@ -24,4 +26,10 @@ public abstract class Enum<E extends Enum<E>> extends Object implements Comparab
|
||||
return ordinal;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
final java.lang.Enum<?> fromDJVM() {
|
||||
throw new UnsupportedOperationException("Dummy implementation");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.lang.Iterable]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.lang.Iterable}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
public interface Iterable<T> extends java.lang.Iterable<T> {
|
||||
@Override
|
||||
|
@ -54,8 +54,7 @@ public class Object {
|
||||
|
||||
private static Class<?> fromDJVM(Class<?> type) {
|
||||
try {
|
||||
java.lang.String name = type.getName();
|
||||
return Class.forName(name.startsWith("sandbox.") ? name.substring(8) : name);
|
||||
return DJVM.fromDJVMType(type);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuleViolationError(e.getMessage());
|
||||
}
|
||||
|
46
djvm/src/main/java/sandbox/java/lang/StackTraceElement.java
Normal file
46
djvm/src/main/java/sandbox/java/lang/StackTraceElement.java
Normal file
@ -0,0 +1,46 @@
|
||||
package sandbox.java.lang;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* This is a dummy class. We will load the genuine class at runtime.
|
||||
*/
|
||||
public final class StackTraceElement extends Object implements java.io.Serializable {
|
||||
|
||||
private final String className;
|
||||
private final String methodName;
|
||||
private final String fileName;
|
||||
private final int lineNumber;
|
||||
|
||||
public StackTraceElement(String className, String methodName, String fileName, int lineNumber) {
|
||||
this.className = className;
|
||||
this.methodName = methodName;
|
||||
this.fileName = fileName;
|
||||
this.lineNumber = lineNumber;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return className;
|
||||
}
|
||||
|
||||
public String getMethodName() {
|
||||
return methodName;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public int getLineNumber() {
|
||||
return lineNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String toDJVMString() {
|
||||
return String.toDJVM(
|
||||
className.toString() + ':' + methodName.toString()
|
||||
+ (fileName != null ? '(' + fileName.toString() + ':' + lineNumber + ')' : "")
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ import sandbox.java.util.Locale;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class String extends Object implements Comparable<String>, CharSequence, Serializable {
|
||||
@ -22,6 +24,18 @@ public final class String extends Object implements Comparable<String>, CharSequ
|
||||
private static final String TRUE = new String("true");
|
||||
private static final String FALSE = new String("false");
|
||||
|
||||
private static final Map<java.lang.String, String> INTERNAL = new java.util.HashMap<>();
|
||||
private static final Constructor SHARED;
|
||||
|
||||
static {
|
||||
try {
|
||||
SHARED = java.lang.String.class.getDeclaredConstructor(char[].class, java.lang.Boolean.TYPE);
|
||||
SHARED.setAccessible(true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new NoSuchMethodError(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private final java.lang.String value;
|
||||
|
||||
public String() {
|
||||
@ -88,6 +102,17 @@ public final class String extends Object implements Comparable<String>, CharSequ
|
||||
this.value = builder.toString();
|
||||
}
|
||||
|
||||
String(char[] value, boolean share) {
|
||||
java.lang.String newValue;
|
||||
try {
|
||||
// This is (presumably) an optimisation for memory usage.
|
||||
newValue = (java.lang.String) SHARED.newInstance(value, share);
|
||||
} catch (Exception e) {
|
||||
newValue = new java.lang.String(value);
|
||||
}
|
||||
this.value = newValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
return value.charAt(index);
|
||||
@ -310,6 +335,8 @@ public final class String extends Object implements Comparable<String>, CharSequ
|
||||
return toDJVM(value.trim());
|
||||
}
|
||||
|
||||
public String intern() { return INTERNAL.computeIfAbsent(value, s -> this); }
|
||||
|
||||
public char[] toCharArray() {
|
||||
return value.toCharArray();
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package sandbox.java.lang;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.lang.StringBuffer]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.lang.StringBuffer}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
public abstract class StringBuffer extends Object implements CharSequence, Appendable, Serializable {
|
||||
|
||||
|
@ -3,8 +3,8 @@ package sandbox.java.lang;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.lang.StringBuilder]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.lang.StringBuilder}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
public abstract class StringBuilder extends Object implements Appendable, CharSequence, Serializable {
|
||||
|
||||
|
137
djvm/src/main/java/sandbox/java/lang/Throwable.java
Normal file
137
djvm/src/main/java/sandbox/java/lang/Throwable.java
Normal file
@ -0,0 +1,137 @@
|
||||
package sandbox.java.lang;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import sandbox.TaskTypes;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
public class Throwable extends Object implements Serializable {
|
||||
private static final StackTraceElement[] NO_STACK_TRACE = new StackTraceElement[0];
|
||||
|
||||
private String message;
|
||||
private Throwable cause;
|
||||
private StackTraceElement[] stackTrace;
|
||||
|
||||
public Throwable() {
|
||||
this.cause = this;
|
||||
fillInStackTrace();
|
||||
}
|
||||
|
||||
public Throwable(String message) {
|
||||
this();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Throwable(Throwable cause) {
|
||||
this.cause = cause;
|
||||
this.message = (cause == null) ? null : cause.toDJVMString();
|
||||
fillInStackTrace();
|
||||
}
|
||||
|
||||
public Throwable(String message, Throwable cause) {
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
fillInStackTrace();
|
||||
}
|
||||
|
||||
protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
if (writableStackTrace) {
|
||||
fillInStackTrace();
|
||||
} else {
|
||||
stackTrace = NO_STACK_TRACE;
|
||||
}
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getLocalizedMessage() {
|
||||
return getMessage();
|
||||
}
|
||||
|
||||
public Throwable getCause() {
|
||||
return (cause == this) ? null : cause;
|
||||
}
|
||||
|
||||
public Throwable initCause(Throwable cause) {
|
||||
if (this.cause != this) {
|
||||
throw new java.lang.IllegalStateException(
|
||||
"Can't overwrite cause with " + java.util.Objects.toString(cause, "a null"), fromDJVM());
|
||||
}
|
||||
if (cause == this) {
|
||||
throw new java.lang.IllegalArgumentException("Self-causation not permitted", fromDJVM());
|
||||
}
|
||||
this.cause = cause;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String toDJVMString() {
|
||||
java.lang.String s = getClass().getName();
|
||||
String localized = getLocalizedMessage();
|
||||
return String.valueOf((localized != null) ? (s + ": " + localized.toString()) : s);
|
||||
}
|
||||
|
||||
public StackTraceElement[] getStackTrace() {
|
||||
return (stackTrace == NO_STACK_TRACE) ? stackTrace : stackTrace.clone();
|
||||
}
|
||||
|
||||
public void setStackTrace(StackTraceElement[] stackTrace) {
|
||||
StackTraceElement[] traceCopy = stackTrace.clone();
|
||||
|
||||
for (int i = 0; i < traceCopy.length; ++i) {
|
||||
if (traceCopy[i] == null) {
|
||||
throw new java.lang.NullPointerException("stackTrace[" + i + ']');
|
||||
}
|
||||
}
|
||||
|
||||
this.stackTrace = traceCopy;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"ThrowableNotThrown", "UnusedReturnValue"})
|
||||
public Throwable fillInStackTrace() {
|
||||
if (stackTrace == null) {
|
||||
/*
|
||||
* We have been invoked from within this exception's constructor.
|
||||
* Work our way up the stack trace until we find this constructor,
|
||||
* and then find out who actually invoked it. This is where our
|
||||
* sandboxed stack trace will start from.
|
||||
*
|
||||
* Our stack trace will end at the point where we entered the sandbox.
|
||||
*/
|
||||
final java.lang.StackTraceElement[] elements = new java.lang.Throwable().getStackTrace();
|
||||
final java.lang.String exceptionName = getClass().getName();
|
||||
int startIdx = 1;
|
||||
while (startIdx < elements.length && !isConstructorFor(elements[startIdx], exceptionName)) {
|
||||
++startIdx;
|
||||
}
|
||||
while (startIdx < elements.length && isConstructorFor(elements[startIdx], exceptionName)) {
|
||||
++startIdx;
|
||||
}
|
||||
|
||||
int endIdx = startIdx;
|
||||
while (endIdx < elements.length && !TaskTypes.isEntryPoint(elements[endIdx])) {
|
||||
++endIdx;
|
||||
}
|
||||
stackTrace = (startIdx == elements.length) ? NO_STACK_TRACE : DJVM.copyToDJVM(elements, startIdx, endIdx);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private static boolean isConstructorFor(java.lang.StackTraceElement elt, java.lang.String className) {
|
||||
return elt.getClassName().equals(className) && elt.getMethodName().equals("<init>");
|
||||
}
|
||||
|
||||
public void printStackTrace() {}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
java.lang.Throwable fromDJVM() {
|
||||
return DJVM.fromDJVM(this);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package sandbox.java.nio.charset;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.nio.charset.Charset]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.nio.charset.Charset}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class Charset extends sandbox.java.lang.Object {
|
||||
|
@ -1,8 +1,8 @@
|
||||
package sandbox.java.util;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.util.Comparator]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.util.Comparator}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Comparator<T> extends java.util.Comparator<T> {
|
||||
|
@ -1,8 +1,8 @@
|
||||
package sandbox.java.util;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.util.Locale]
|
||||
* to allow us to compile [sandbox.java.lang.String].
|
||||
* This is a dummy class that implements just enough of {@link java.util.Locale}
|
||||
* to allow us to compile {@link sandbox.java.lang.String}.
|
||||
*/
|
||||
public abstract class Locale extends sandbox.java.lang.Object {
|
||||
public abstract sandbox.java.lang.String toLanguageTag();
|
||||
|
@ -1,8 +1,8 @@
|
||||
package sandbox.java.util.function;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.util.function.Function]
|
||||
* to allow us to compile [sandbox.Task].
|
||||
* This is a dummy class that implements just enough of {@link java.util.function.Function}
|
||||
* to allow us to compile {@link sandbox.Task}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Function<T, R> {
|
||||
|
@ -1,8 +1,8 @@
|
||||
package sandbox.java.util.function;
|
||||
|
||||
/**
|
||||
* This is a dummy class that implements just enough of [java.util.function.Supplier]
|
||||
* to allow us to compile [sandbox.java.lang.ThreadLocal].
|
||||
* This is a dummy class that implements just enough of @{link java.util.function.Supplier}
|
||||
* to allow us to compile {@link sandbox.java.lang.ThreadLocal}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Supplier<T> {
|
||||
|
@ -2,6 +2,7 @@ package net.corda.djvm
|
||||
|
||||
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||
import net.corda.djvm.code.DefinitionProvider
|
||||
import net.corda.djvm.code.EMIT_TRACING
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.execution.ExecutionProfile
|
||||
import net.corda.djvm.rules.Rule
|
||||
@ -51,7 +52,7 @@ class SandboxConfiguration private constructor(
|
||||
executionProfile = profile,
|
||||
rules = rules,
|
||||
emitters = (emitters ?: Discovery.find()).filter {
|
||||
enableTracing || !it.isTracer
|
||||
enableTracing || it.priority > EMIT_TRACING
|
||||
},
|
||||
definitionProviders = definitionProviders,
|
||||
analysisConfiguration = analysisConfiguration
|
||||
|
@ -58,11 +58,21 @@ class AnalysisConfiguration(
|
||||
*/
|
||||
val stitchedInterfaces: Map<String, List<Member>> get() = STITCHED_INTERFACES
|
||||
|
||||
/**
|
||||
* These classes have extra methods added as they are mapped into the sandbox.
|
||||
*/
|
||||
val stitchedClasses: Map<String, List<Member>> get() = STITCHED_CLASSES
|
||||
|
||||
/**
|
||||
* Functionality used to resolve the qualified name and relevant information about a class.
|
||||
*/
|
||||
val classResolver: ClassResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX)
|
||||
|
||||
/**
|
||||
* Resolves the internal names of synthetic exception classes.
|
||||
*/
|
||||
val exceptionResolver: ExceptionResolver = ExceptionResolver(JVM_EXCEPTIONS, pinnedClasses, SANDBOX_PREFIX)
|
||||
|
||||
private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) }
|
||||
val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader)
|
||||
|
||||
@ -76,11 +86,14 @@ class AnalysisConfiguration(
|
||||
fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES
|
||||
fun isPinnedClass(className: String): Boolean = className in pinnedClasses
|
||||
|
||||
fun isJvmException(className: String): Boolean = className in JVM_EXCEPTIONS
|
||||
fun isSandboxClass(className: String): Boolean = className.startsWith(SANDBOX_PREFIX) && !isPinnedClass(className)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The package name prefix to use for classes loaded into a sandbox.
|
||||
*/
|
||||
private const val SANDBOX_PREFIX: String = "sandbox/"
|
||||
const val SANDBOX_PREFIX: String = "sandbox/"
|
||||
|
||||
/**
|
||||
* These class must belong to the application class loader.
|
||||
@ -111,27 +124,90 @@ class AnalysisConfiguration(
|
||||
java.lang.String.CASE_INSENSITIVE_ORDER::class.java,
|
||||
java.lang.System::class.java,
|
||||
java.lang.ThreadLocal::class.java,
|
||||
java.lang.Throwable::class.java,
|
||||
kotlin.Any::class.java,
|
||||
sun.misc.JavaLangAccess::class.java,
|
||||
sun.misc.SharedSecrets::class.java
|
||||
).sandboxed() + setOf(
|
||||
"sandbox/Task",
|
||||
"sandbox/TaskTypes",
|
||||
"sandbox/java/lang/DJVM",
|
||||
"sandbox/java/lang/DJVMException",
|
||||
"sandbox/java/lang/DJVMThrowableWrapper",
|
||||
"sandbox/sun/misc/SharedSecrets\$1",
|
||||
"sandbox/sun/misc/SharedSecrets\$JavaLangAccessImpl"
|
||||
)
|
||||
|
||||
/**
|
||||
* These are thrown by the JVM itself, and so
|
||||
* we need to handle them without wrapping them.
|
||||
*
|
||||
* Note that this set is closed, i.e. every one
|
||||
* of these exceptions' [Throwable] super classes
|
||||
* is also within this set.
|
||||
*
|
||||
* The full list of exceptions is determined by:
|
||||
* hotspot/src/share/vm/classfile/vmSymbols.hpp
|
||||
*/
|
||||
val JVM_EXCEPTIONS: Set<String> = setOf(
|
||||
java.io.IOException::class.java,
|
||||
java.lang.AbstractMethodError::class.java,
|
||||
java.lang.ArithmeticException::class.java,
|
||||
java.lang.ArrayIndexOutOfBoundsException::class.java,
|
||||
java.lang.ArrayStoreException::class.java,
|
||||
java.lang.ClassCastException::class.java,
|
||||
java.lang.ClassCircularityError::class.java,
|
||||
java.lang.ClassFormatError::class.java,
|
||||
java.lang.ClassNotFoundException::class.java,
|
||||
java.lang.CloneNotSupportedException::class.java,
|
||||
java.lang.Error::class.java,
|
||||
java.lang.Exception::class.java,
|
||||
java.lang.ExceptionInInitializerError::class.java,
|
||||
java.lang.IllegalAccessError::class.java,
|
||||
java.lang.IllegalAccessException::class.java,
|
||||
java.lang.IllegalArgumentException::class.java,
|
||||
java.lang.IllegalStateException::class.java,
|
||||
java.lang.IncompatibleClassChangeError::class.java,
|
||||
java.lang.IndexOutOfBoundsException::class.java,
|
||||
java.lang.InstantiationError::class.java,
|
||||
java.lang.InstantiationException::class.java,
|
||||
java.lang.InternalError::class.java,
|
||||
java.lang.LinkageError::class.java,
|
||||
java.lang.NegativeArraySizeException::class.java,
|
||||
java.lang.NoClassDefFoundError::class.java,
|
||||
java.lang.NoSuchFieldError::class.java,
|
||||
java.lang.NoSuchFieldException::class.java,
|
||||
java.lang.NoSuchMethodError::class.java,
|
||||
java.lang.NoSuchMethodException::class.java,
|
||||
java.lang.NullPointerException::class.java,
|
||||
java.lang.OutOfMemoryError::class.java,
|
||||
java.lang.ReflectiveOperationException::class.java,
|
||||
java.lang.RuntimeException::class.java,
|
||||
java.lang.StackOverflowError::class.java,
|
||||
java.lang.StringIndexOutOfBoundsException::class.java,
|
||||
java.lang.ThreadDeath::class.java,
|
||||
java.lang.Throwable::class.java,
|
||||
java.lang.UnknownError::class.java,
|
||||
java.lang.UnsatisfiedLinkError::class.java,
|
||||
java.lang.UnsupportedClassVersionError::class.java,
|
||||
java.lang.UnsupportedOperationException::class.java,
|
||||
java.lang.VerifyError::class.java,
|
||||
java.lang.VirtualMachineError::class.java
|
||||
).sandboxed() + setOf(
|
||||
// Mentioned here to prevent the DJVM from generating a synthetic wrapper.
|
||||
"sandbox/java/lang/DJVMThrowableWrapper"
|
||||
)
|
||||
|
||||
/**
|
||||
* These interfaces will be modified as follows when
|
||||
* added to the sandbox:
|
||||
*
|
||||
* <code>interface sandbox.A extends A</code>
|
||||
*/
|
||||
private val STITCHED_INTERFACES: Map<String, List<Member>> = mapOf(
|
||||
sandboxed(CharSequence::class.java) to listOf(
|
||||
private val STITCHED_INTERFACES: Map<String, List<Member>> = listOf(
|
||||
object : MethodBuilder(
|
||||
access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE,
|
||||
className = "sandbox/java/lang/CharSequence",
|
||||
className = sandboxed(CharSequence::class.java),
|
||||
memberName = "subSequence",
|
||||
descriptor = "(II)Ljava/lang/CharSequence;"
|
||||
) {
|
||||
@ -144,27 +220,66 @@ class AnalysisConfiguration(
|
||||
}
|
||||
}.withBody()
|
||||
.build(),
|
||||
|
||||
MethodBuilder(
|
||||
access = ACC_PUBLIC or ACC_ABSTRACT,
|
||||
className = "sandbox/java/lang/CharSequence",
|
||||
className = sandboxed(CharSequence::class.java),
|
||||
memberName = "toString",
|
||||
descriptor = "()Ljava/lang/String;"
|
||||
).build()
|
||||
),
|
||||
).mapByClassName() + mapOf(
|
||||
sandboxed(Comparable::class.java) to emptyList(),
|
||||
sandboxed(Comparator::class.java) to emptyList(),
|
||||
sandboxed(Iterable::class.java) to emptyList()
|
||||
)
|
||||
|
||||
private fun sandboxed(clazz: Class<*>) = SANDBOX_PREFIX + Type.getInternalName(clazz)
|
||||
/**
|
||||
* These classes have extra methods added when mapped into the sandbox.
|
||||
*/
|
||||
private val STITCHED_CLASSES: Map<String, List<Member>> = listOf(
|
||||
object : MethodBuilder(
|
||||
access = ACC_FINAL,
|
||||
className = sandboxed(Enum::class.java),
|
||||
memberName = "fromDJVM",
|
||||
descriptor = "()Ljava/lang/Enum;",
|
||||
signature = "()Ljava/lang/Enum<*>;"
|
||||
) {
|
||||
override fun writeBody(emitter: EmitterModule) = with(emitter) {
|
||||
pushObject(0)
|
||||
invokeStatic("sandbox/java/lang/DJVM", "fromDJVMEnum", "(Lsandbox/java/lang/Enum;)Ljava/lang/Enum;")
|
||||
returnObject()
|
||||
}
|
||||
}.withBody()
|
||||
.build(),
|
||||
|
||||
object : MethodBuilder(
|
||||
access = ACC_BRIDGE or ACC_SYNTHETIC,
|
||||
className = sandboxed(Enum::class.java),
|
||||
memberName = "fromDJVM",
|
||||
descriptor = "()Ljava/lang/Object;"
|
||||
) {
|
||||
override fun writeBody(emitter: EmitterModule) = with(emitter) {
|
||||
pushObject(0)
|
||||
invokeVirtual(className, memberName, "()Ljava/lang/Enum;")
|
||||
returnObject()
|
||||
}
|
||||
}.withBody()
|
||||
.build()
|
||||
).mapByClassName()
|
||||
|
||||
private fun sandboxed(clazz: Class<*>): String = (SANDBOX_PREFIX + Type.getInternalName(clazz)).intern()
|
||||
private fun Set<Class<*>>.sandboxed(): Set<String> = map(Companion::sandboxed).toSet()
|
||||
private fun Iterable<Member>.mapByClassName(): Map<String, List<Member>>
|
||||
= groupBy(Member::className).mapValues(Map.Entry<String, List<Member>>::value)
|
||||
}
|
||||
|
||||
private open class MethodBuilder(
|
||||
protected val access: Int,
|
||||
protected val className: String,
|
||||
protected val memberName: String,
|
||||
protected val descriptor: String) {
|
||||
protected val descriptor: String,
|
||||
protected val signature: String = ""
|
||||
) {
|
||||
private val bodies = mutableListOf<MethodBody>()
|
||||
|
||||
protected open fun writeBody(emitter: EmitterModule) {}
|
||||
@ -179,7 +294,7 @@ class AnalysisConfiguration(
|
||||
className = className,
|
||||
memberName = memberName,
|
||||
signature = descriptor,
|
||||
genericsDetails = "",
|
||||
genericsDetails = signature,
|
||||
body = bodies
|
||||
)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.djvm.analysis
|
||||
|
||||
import net.corda.djvm.code.EmitterModule
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.emptyAsNull
|
||||
import net.corda.djvm.code.instructions.*
|
||||
import net.corda.djvm.messages.Message
|
||||
import net.corda.djvm.references.*
|
||||
@ -232,7 +233,7 @@ open class ClassAndMemberVisitor(
|
||||
analysisContext.classes.add(visitedClass)
|
||||
super.visit(
|
||||
version, access, visitedClass.name, signature,
|
||||
visitedClass.superClass.nullIfEmpty(),
|
||||
visitedClass.superClass.emptyAsNull,
|
||||
visitedClass.interfaces.toTypedArray()
|
||||
)
|
||||
}
|
||||
@ -285,7 +286,14 @@ open class ClassAndMemberVisitor(
|
||||
): MethodVisitor? {
|
||||
var visitedMember: Member? = null
|
||||
val clazz = currentClass!!
|
||||
val member = Member(access, clazz.name, name, desc, signature ?: "")
|
||||
val member = Member(
|
||||
access = access,
|
||||
className = clazz.name,
|
||||
memberName = name,
|
||||
signature = desc,
|
||||
genericsDetails = signature ?: "",
|
||||
exceptions = exceptions?.toMutableSet() ?: mutableSetOf()
|
||||
)
|
||||
currentMember = member
|
||||
sourceLocation = sourceLocation.copy(
|
||||
memberName = name,
|
||||
@ -320,7 +328,14 @@ open class ClassAndMemberVisitor(
|
||||
): FieldVisitor? {
|
||||
var visitedMember: Member? = null
|
||||
val clazz = currentClass!!
|
||||
val member = Member(access, clazz.name, name, desc, "", value = value)
|
||||
val member = Member(
|
||||
access = access,
|
||||
className = clazz.name,
|
||||
memberName = name,
|
||||
signature = desc,
|
||||
genericsDetails = "",
|
||||
value = value
|
||||
)
|
||||
currentMember = member
|
||||
sourceLocation = sourceLocation.copy(
|
||||
memberName = name,
|
||||
@ -578,10 +593,6 @@ open class ClassAndMemberVisitor(
|
||||
*/
|
||||
const val API_VERSION: Int = Opcodes.ASM6
|
||||
|
||||
private fun String.nullIfEmpty(): String? {
|
||||
return if (this.isEmpty()) { null } else { this }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
package net.corda.djvm.analysis
|
||||
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
class ExceptionResolver(
|
||||
private val jvmExceptionClasses: Set<String>,
|
||||
private val pinnedClasses: Set<String>,
|
||||
private val sandboxPrefix: String
|
||||
) {
|
||||
companion object {
|
||||
private const val DJVM_EXCEPTION_NAME = "\$1DJVM"
|
||||
|
||||
fun isDJVMException(className: String): Boolean = className.endsWith(DJVM_EXCEPTION_NAME)
|
||||
fun getDJVMException(className: String): String = className + DJVM_EXCEPTION_NAME
|
||||
fun getDJVMExceptionOwner(className: String): String = className.dropLast(DJVM_EXCEPTION_NAME.length)
|
||||
}
|
||||
|
||||
fun getThrowableName(clazz: Class<*>): String {
|
||||
return getDJVMException(Type.getInternalName(clazz))
|
||||
}
|
||||
|
||||
fun getThrowableSuperName(clazz: Class<*>): String {
|
||||
return getThrowableOwnerName(Type.getInternalName(clazz.superclass))
|
||||
}
|
||||
|
||||
fun getThrowableOwnerName(className: String): String {
|
||||
return if (className in jvmExceptionClasses) {
|
||||
className.unsandboxed
|
||||
} else if (className in pinnedClasses) {
|
||||
className
|
||||
} else {
|
||||
getDJVMException(className)
|
||||
}
|
||||
}
|
||||
|
||||
private val String.unsandboxed: String get() = if (startsWith(sandboxPrefix)) {
|
||||
drop(sandboxPrefix.length)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
@ -100,9 +100,6 @@ open class Whitelist private constructor(
|
||||
"^java/lang/Cloneable(\\..*)?\$".toRegex(),
|
||||
"^java/lang/Object(\\..*)?\$".toRegex(),
|
||||
"^java/lang/Override(\\..*)?\$".toRegex(),
|
||||
// TODO: sandbox exception handling!
|
||||
"^java/lang/StackTraceElement\$".toRegex(),
|
||||
"^java/lang/Throwable\$".toRegex(),
|
||||
"^java/lang/Void\$".toRegex(),
|
||||
"^java/lang/invoke/LambdaMetafactory\$".toRegex(),
|
||||
"^java/lang/invoke/MethodHandles(\\\$.*)?\$".toRegex(),
|
||||
|
@ -41,7 +41,11 @@ class ClassMutator(
|
||||
}
|
||||
}
|
||||
|
||||
private val emitters: List<Emitter> = emitters + PrependClassInitializer()
|
||||
/*
|
||||
* Some emitters must be executed before others. E.g. we need to apply
|
||||
* the tracing emitters before the non-tracing ones.
|
||||
*/
|
||||
private val emitters: List<Emitter> = (emitters + PrependClassInitializer()).sortedBy(Emitter::priority)
|
||||
private val initializers = mutableListOf<MethodBody>()
|
||||
|
||||
/**
|
||||
@ -128,8 +132,7 @@ class ClassMutator(
|
||||
*/
|
||||
override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) {
|
||||
val context = EmitterContext(currentAnalysisContext(), configuration, emitter)
|
||||
// We need to apply the tracing emitters before the non-tracing ones.
|
||||
Processor.processEntriesOfType<Emitter>(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) {
|
||||
Processor.processEntriesOfType<Emitter>(emitters, analysisContext.messages) {
|
||||
it.emit(context, instruction)
|
||||
}
|
||||
if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) {
|
||||
|
@ -18,10 +18,10 @@ interface Emitter {
|
||||
fun emit(context: EmitterContext, instruction: Instruction)
|
||||
|
||||
/**
|
||||
* Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox.
|
||||
* Determines the order in which emitters are executed within the sandbox.
|
||||
*/
|
||||
@JvmDefault
|
||||
val isTracer: Boolean
|
||||
get() = false
|
||||
val priority: Int
|
||||
get() = EMIT_DEFAULT
|
||||
|
||||
}
|
@ -168,6 +168,14 @@ class EmitterModule(
|
||||
|
||||
inline fun <reified T : Throwable> throwException(message: String) = throwException(T::class.java, message)
|
||||
|
||||
/**
|
||||
* Attempt to cast the object on the top of the stack to the given class.
|
||||
*/
|
||||
fun castObjectTo(className: String) {
|
||||
methodVisitor.visitTypeInsn(CHECKCAST, className)
|
||||
hasEmittedCustomCode = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit instruction for returning from "void" method.
|
||||
*/
|
||||
|
@ -2,11 +2,22 @@
|
||||
package net.corda.djvm.code
|
||||
|
||||
import org.objectweb.asm.Type
|
||||
import sandbox.java.lang.DJVMException
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
|
||||
/**
|
||||
* These are the priorities for executing [Emitter] instances.
|
||||
* Tracing emitters are executed first.
|
||||
*/
|
||||
const val EMIT_TRACING: Int = 0
|
||||
const val EMIT_TRAPPING_EXCEPTIONS: Int = EMIT_TRACING + 1
|
||||
const val EMIT_HANDLING_EXCEPTIONS: Int = EMIT_TRAPPING_EXCEPTIONS + 1
|
||||
const val EMIT_DEFAULT: Int = 10
|
||||
|
||||
val ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java)
|
||||
val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::class.java)
|
||||
val djvmException: String = Type.getInternalName(DJVMException::class.java)
|
||||
|
||||
/**
|
||||
* Local extension method for normalizing a class name.
|
||||
|
@ -0,0 +1,8 @@
|
||||
package net.corda.djvm.code.instructions
|
||||
|
||||
import org.objectweb.asm.Label
|
||||
|
||||
open class TryBlock(
|
||||
val handler: Label,
|
||||
val typeName: String
|
||||
) : NoOperationInstruction()
|
@ -9,6 +9,6 @@ import org.objectweb.asm.Label
|
||||
* @property handler The label of the exception handler.
|
||||
*/
|
||||
class TryCatchBlock(
|
||||
val typeName: String,
|
||||
val handler: Label
|
||||
) : NoOperationInstruction()
|
||||
typeName: String,
|
||||
handler: Label
|
||||
) : TryBlock(handler, typeName)
|
||||
|
@ -7,7 +7,6 @@ import org.objectweb.asm.Label
|
||||
*
|
||||
* @property handler The handler for the finally-block.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class TryFinallyBlock(
|
||||
val handler: Label
|
||||
) : NoOperationInstruction()
|
||||
handler: Label
|
||||
) : TryBlock(handler, "")
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.djvm.rewiring
|
||||
|
||||
import net.corda.djvm.SandboxConfiguration
|
||||
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION
|
||||
import net.corda.djvm.code.ClassMutator
|
||||
@ -11,6 +10,8 @@ import net.corda.djvm.references.Member
|
||||
import net.corda.djvm.utilities.loggerFor
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.Label
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
|
||||
/**
|
||||
* Functionality for rewriting parts of a class as it is being loaded.
|
||||
@ -22,6 +23,7 @@ open class ClassRewriter(
|
||||
private val configuration: SandboxConfiguration,
|
||||
private val classLoader: ClassLoader
|
||||
) {
|
||||
private val analysisConfig = configuration.analysisConfiguration
|
||||
|
||||
/**
|
||||
* Process class and allow user to rewrite parts/all of its content through provided hooks.
|
||||
@ -32,11 +34,13 @@ open class ClassRewriter(
|
||||
fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode {
|
||||
logger.debug("Rewriting class {}...", reader.className)
|
||||
val writer = SandboxClassWriter(reader, classLoader)
|
||||
val analysisConfiguration = configuration.analysisConfiguration
|
||||
val classRemapper = SandboxClassRemapper(InterfaceStitcher(writer, analysisConfiguration), analysisConfiguration)
|
||||
val classRemapper = SandboxClassRemapper(
|
||||
ClassExceptionRemapper(SandboxStitcher(writer)),
|
||||
analysisConfig
|
||||
)
|
||||
val visitor = ClassMutator(
|
||||
classRemapper,
|
||||
analysisConfiguration,
|
||||
analysisConfig,
|
||||
configuration.definitionProviders,
|
||||
configuration.emitters
|
||||
)
|
||||
@ -50,25 +54,30 @@ open class ClassRewriter(
|
||||
|
||||
/**
|
||||
* Extra visitor that is applied after [SandboxRemapper]. This "stitches" the original
|
||||
* unmapped interface as a super-interface of the mapped version.
|
||||
* unmapped interface as a super-interface of the mapped version, as well as adding
|
||||
* any extra methods that are needed.
|
||||
*/
|
||||
private class InterfaceStitcher(parent: ClassVisitor, private val configuration: AnalysisConfiguration)
|
||||
private inner class SandboxStitcher(parent: ClassVisitor)
|
||||
: ClassVisitor(API_VERSION, parent)
|
||||
{
|
||||
private val extraMethods = mutableListOf<Member>()
|
||||
|
||||
override fun visit(version: Int, access: Int, className: String, signature: String?, superName: String?, interfaces: Array<String>?) {
|
||||
val stitchedInterfaces = configuration.stitchedInterfaces[className]?.let { methods ->
|
||||
val stitchedInterfaces = analysisConfig.stitchedInterfaces[className]?.let { methods ->
|
||||
extraMethods += methods
|
||||
arrayOf(*(interfaces ?: emptyArray()), configuration.classResolver.reverse(className))
|
||||
arrayOf(*(interfaces ?: emptyArray()), analysisConfig.classResolver.reverse(className))
|
||||
} ?: interfaces
|
||||
|
||||
analysisConfig.stitchedClasses[className]?.also { methods ->
|
||||
extraMethods += methods
|
||||
}
|
||||
|
||||
super.visit(version, access, className, signature, superName, stitchedInterfaces)
|
||||
}
|
||||
|
||||
override fun visitEnd() {
|
||||
for (method in extraMethods) {
|
||||
method.apply {
|
||||
with(method) {
|
||||
visitMethod(access, memberName, signature, genericsDetails.emptyAsNull, exceptions.toTypedArray())?.also { mv ->
|
||||
mv.visitCode()
|
||||
EmitterModule(mv).writeByteCode(body)
|
||||
@ -81,4 +90,26 @@ open class ClassRewriter(
|
||||
super.visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map exceptions in method signatures to their sandboxed equivalents.
|
||||
*/
|
||||
private inner class ClassExceptionRemapper(parent: ClassVisitor) : ClassVisitor(API_VERSION, parent) {
|
||||
override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
|
||||
val mappedExceptions = exceptions?.map(analysisConfig.exceptionResolver::getThrowableOwnerName)?.toTypedArray()
|
||||
return super.visitMethod(access, name, descriptor, signature, mappedExceptions)?.let {
|
||||
MethodExceptionRemapper(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map exceptions in method try-catch blocks to their sandboxed equivalents.
|
||||
*/
|
||||
private inner class MethodExceptionRemapper(parent: MethodVisitor) : MethodVisitor(API_VERSION, parent) {
|
||||
override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, exceptionType: String?) {
|
||||
val mappedExceptionType = exceptionType?.let(analysisConfig.exceptionResolver::getThrowableOwnerName)
|
||||
super.visitTryCatchBlock(start, end, handler, mappedExceptionType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ package net.corda.djvm.rewiring
|
||||
import net.corda.djvm.SandboxConfiguration
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.analysis.ClassAndMemberVisitor
|
||||
import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner
|
||||
import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException
|
||||
import net.corda.djvm.code.asPackagePath
|
||||
import net.corda.djvm.code.asResourcePath
|
||||
import net.corda.djvm.references.ClassReference
|
||||
import net.corda.djvm.source.ClassSource
|
||||
@ -33,7 +36,7 @@ class SandboxClassLoader(
|
||||
/**
|
||||
* The analyzer used to traverse the class hierarchy.
|
||||
*/
|
||||
val analyzer: ClassAndMemberVisitor
|
||||
private val analyzer: ClassAndMemberVisitor
|
||||
get() = ruleValidator
|
||||
|
||||
/**
|
||||
@ -56,6 +59,18 @@ class SandboxClassLoader(
|
||||
*/
|
||||
private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader)
|
||||
|
||||
/**
|
||||
* We need to load this class up front, so that we can identify sandboxed exception classes.
|
||||
*/
|
||||
private val throwableClass: Class<*>
|
||||
|
||||
init {
|
||||
// Bootstrap the loading of the sandboxed Throwable class.
|
||||
loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Object"), context)
|
||||
loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.StackTraceElement"), context)
|
||||
throwableClass = loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Throwable"), context).type
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a class name, provide its corresponding [LoadedClass] for the sandbox.
|
||||
*/
|
||||
@ -77,13 +92,45 @@ class SandboxClassLoader(
|
||||
*/
|
||||
@Throws(ClassNotFoundException::class)
|
||||
override fun loadClass(name: String, resolve: Boolean): Class<*> {
|
||||
var clazz = findLoadedClass(name)
|
||||
if (clazz == null) {
|
||||
val source = ClassSource.fromClassName(name)
|
||||
return if (name.startsWith("sandbox.") && !analysisConfiguration.isPinnedClass(source.internalClassName)) {
|
||||
loadClassAndBytes(source, context).type
|
||||
clazz = if (analysisConfiguration.isSandboxClass(source.internalClassName)) {
|
||||
loadSandboxClass(source, context).type
|
||||
} else {
|
||||
super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
if (resolve) {
|
||||
resolveClass(clazz)
|
||||
}
|
||||
return clazz
|
||||
}
|
||||
|
||||
private fun loadSandboxClass(source: ClassSource, context: AnalysisContext): LoadedClass {
|
||||
return if (isDJVMException(source.internalClassName)) {
|
||||
/**
|
||||
* We need to load a DJVMException's owner class before we can create
|
||||
* its wrapper exception. And loading the owner should also create the
|
||||
* wrapper class automatically.
|
||||
*/
|
||||
loadedClasses.getOrElse(source.internalClassName) {
|
||||
loadSandboxClass(ClassSource.fromClassName(getDJVMExceptionOwner(source.qualifiedClassName)), context)
|
||||
loadedClasses[source.internalClassName]
|
||||
} ?: throw ClassNotFoundException(source.qualifiedClassName)
|
||||
} else {
|
||||
loadClassAndBytes(source, context).also { clazz ->
|
||||
/**
|
||||
* Check whether we've just loaded an unpinned sandboxed throwable class.
|
||||
* If we have, we may also need to synthesise a throwable wrapper for it.
|
||||
*/
|
||||
if (throwableClass.isAssignableFrom(clazz.type) && !analysisConfiguration.isJvmException(source.internalClassName)) {
|
||||
logger.debug("Generating synthetic throwable for ${source.qualifiedClassName}")
|
||||
loadWrapperFor(clazz.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the class with the specified binary name.
|
||||
@ -134,7 +181,7 @@ class SandboxClassLoader(
|
||||
}
|
||||
|
||||
// Try to define the transformed class.
|
||||
val clazz = try {
|
||||
val clazz: Class<*> = try {
|
||||
when {
|
||||
whitelistedClasses.matches(sourceName.asResourcePath) -> supportingClassLoader.loadClass(sourceName)
|
||||
else -> defineClass(resolvedName, byteCode.bytes, 0, byteCode.bytes.size)
|
||||
@ -167,6 +214,15 @@ class SandboxClassLoader(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadWrapperFor(throwable: Class<*>): LoadedClass {
|
||||
val className = analysisConfiguration.exceptionResolver.getThrowableName(throwable)
|
||||
return loadedClasses.getOrPut(className) {
|
||||
val superName = analysisConfiguration.exceptionResolver.getThrowableSuperName(throwable)
|
||||
val byteCode = ThrowableWrapperFactory.toByteCode(className, superName)
|
||||
LoadedClass(defineClass(className.asPackagePath, byteCode.bytes, 0, byteCode.bytes.size), byteCode)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val logger = loggerFor<SandboxClassLoader>()
|
||||
private val UNMODIFIED = ByteCode(ByteArray(0), false)
|
||||
|
@ -3,7 +3,6 @@ package net.corda.djvm.rewiring
|
||||
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||
import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.Label
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.commons.ClassRemapper
|
||||
|
||||
@ -35,11 +34,6 @@ class SandboxClassRemapper(cv: ClassVisitor, private val configuration: Analysis
|
||||
return mapperFor(method).visitMethodInsn(opcode, owner, name, descriptor, isInterface)
|
||||
}
|
||||
|
||||
override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, type: String?) {
|
||||
// Don't map caught exception names - these could be thrown by the JVM itself.
|
||||
nonmapper.visitTryCatchBlock(start, end, handler, type)
|
||||
}
|
||||
|
||||
override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) {
|
||||
val field = Element(owner, name, descriptor)
|
||||
return mapperFor(field).visitFieldInsn(opcode, owner, name, descriptor)
|
||||
|
@ -0,0 +1,152 @@
|
||||
package net.corda.djvm.rewiring
|
||||
|
||||
import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException
|
||||
import net.corda.djvm.code.djvmException
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
|
||||
/**
|
||||
* Generates a synthetic [Throwable] class that will wrap a [sandbox.java.lang.Throwable].
|
||||
* Only exceptions which are NOT thrown by the JVM will be accompanied one of these.
|
||||
*/
|
||||
class ThrowableWrapperFactory(
|
||||
private val className: String,
|
||||
private val superName: String
|
||||
) {
|
||||
companion object {
|
||||
const val CONSTRUCTOR_DESCRIPTOR = "(Lsandbox/java/lang/Throwable;)V"
|
||||
const val FIELD_TYPE = "Lsandbox/java/lang/Throwable;"
|
||||
const val THROWABLE_FIELD = "t"
|
||||
|
||||
fun toByteCode(className: String, superName: String): ByteCode {
|
||||
val bytecode: ByteArray = with(ClassWriter(0)) {
|
||||
ThrowableWrapperFactory(className, superName).accept(this)
|
||||
toByteArray()
|
||||
}
|
||||
return ByteCode(bytecode, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write bytecode for synthetic throwable wrapper class. All of
|
||||
* these classes implement [sandbox.java.lang.DJVMException],
|
||||
* either directly or indirectly.
|
||||
*/
|
||||
fun accept(writer: ClassWriter) = with(writer) {
|
||||
if (isDJVMException(superName)) {
|
||||
childClass()
|
||||
} else {
|
||||
baseClass()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a "base" wrapper class that inherits from a JVM exception.
|
||||
*
|
||||
* <code>
|
||||
* public class CLASSNAME extends JAVA_EXCEPTION implements DJVMException {
|
||||
* private final sandbox.java.lang.Throwable t;
|
||||
*
|
||||
* public CLASSNAME(sandbox.java.lang.Throwable t) {
|
||||
* this.t = t;
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* public final sandbox.java.lang.Throwable getThrowable() {
|
||||
* return t;
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* public final java.lang.Throwable fillInStackTrace() {
|
||||
* return this;
|
||||
* }
|
||||
* }
|
||||
* </code>
|
||||
*/
|
||||
private fun ClassWriter.baseClass() {
|
||||
// Class definition
|
||||
visit(
|
||||
V1_8,
|
||||
ACC_SYNTHETIC or ACC_PUBLIC,
|
||||
className,
|
||||
null,
|
||||
superName,
|
||||
arrayOf(djvmException)
|
||||
)
|
||||
|
||||
// Private final field to hold the sandbox throwable object.
|
||||
visitField(ACC_PRIVATE or ACC_FINAL, THROWABLE_FIELD, FIELD_TYPE, null, null)
|
||||
|
||||
// Constructor
|
||||
visitMethod(ACC_PUBLIC, "<init>", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv ->
|
||||
mv.visitCode()
|
||||
mv.visitVarInsn(ALOAD, 0)
|
||||
mv.visitMethodInsn(INVOKESPECIAL, superName, "<init>", "()V", false)
|
||||
mv.visitVarInsn(ALOAD, 0)
|
||||
mv.visitVarInsn(ALOAD, 1)
|
||||
mv.visitFieldInsn(PUTFIELD, className, THROWABLE_FIELD, FIELD_TYPE)
|
||||
mv.visitInsn(RETURN)
|
||||
mv.visitMaxs(2, 2)
|
||||
mv.visitEnd()
|
||||
}
|
||||
|
||||
// Getter method for the sandbox throwable object.
|
||||
visitMethod(ACC_PUBLIC or ACC_FINAL, "getThrowable", "()$FIELD_TYPE", null, null).also { mv ->
|
||||
mv.visitCode()
|
||||
mv.visitVarInsn(ALOAD, 0)
|
||||
mv.visitFieldInsn(GETFIELD, className, THROWABLE_FIELD, FIELD_TYPE)
|
||||
mv.visitInsn(ARETURN)
|
||||
mv.visitMaxs(1, 1)
|
||||
mv.visitEnd()
|
||||
}
|
||||
|
||||
// Prevent these wrappers from generating their own stack traces.
|
||||
visitMethod(ACC_PUBLIC or ACC_FINAL, "fillInStackTrace", "()Ljava/lang/Throwable;", null, null).also { mv ->
|
||||
mv.visitCode()
|
||||
mv.visitVarInsn(ALOAD, 0)
|
||||
mv.visitInsn(ARETURN)
|
||||
mv.visitMaxs(1, 1)
|
||||
mv.visitEnd()
|
||||
}
|
||||
|
||||
// End of class
|
||||
visitEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* This wrapper class inherits from another wrapper class.
|
||||
*
|
||||
* <code>
|
||||
* public class CLASSNAME extends SUPERNAME {
|
||||
* public CLASSNAME(sandbox.java.lang.Throwable t) {
|
||||
* super(t);
|
||||
* }
|
||||
* }
|
||||
* </code>
|
||||
*/
|
||||
private fun ClassWriter.childClass() {
|
||||
// Class definition
|
||||
visit(
|
||||
V1_8,
|
||||
ACC_SYNTHETIC or ACC_PUBLIC,
|
||||
className,
|
||||
null,
|
||||
superName,
|
||||
arrayOf()
|
||||
)
|
||||
|
||||
// Constructor
|
||||
visitMethod(ACC_PUBLIC, "<init>", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv ->
|
||||
mv.visitCode()
|
||||
mv.visitVarInsn(ALOAD, 0)
|
||||
mv.visitVarInsn(ALOAD, 1)
|
||||
mv.visitMethodInsn(INVOKESPECIAL, superName, "<init>", CONSTRUCTOR_DESCRIPTOR, false)
|
||||
mv.visitInsn(RETURN)
|
||||
mv.visitMaxs(2, 2)
|
||||
mv.visitEnd()
|
||||
}
|
||||
|
||||
// End of class
|
||||
visitEnd()
|
||||
}
|
||||
}
|
@ -53,4 +53,10 @@ class DisallowCatchingBlacklistedExceptions : Emitter {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to invoke this emitter before the [HandleExceptionUnwrapper]
|
||||
* so that we don't unwrap exceptions we don't want to catch.
|
||||
*/
|
||||
override val priority: Int
|
||||
get() = EMIT_TRAPPING_EXCEPTIONS
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.code.EMIT_HANDLING_EXCEPTIONS
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.instructions.CodeLabel
|
||||
import net.corda.djvm.code.instructions.TryBlock
|
||||
import org.objectweb.asm.Label
|
||||
|
||||
/**
|
||||
* Converts an exception from [java.lang.Throwable] to [sandbox.java.lang.Throwable]
|
||||
* at the beginning of either a catch block or a finally block.
|
||||
*/
|
||||
class HandleExceptionUnwrapper : Emitter {
|
||||
private val handlers = mutableMapOf<Label, String>()
|
||||
|
||||
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||
if (instruction is TryBlock) {
|
||||
handlers[instruction.handler] = instruction.typeName
|
||||
} else if (instruction is CodeLabel) {
|
||||
handlers[instruction.label]?.let { exceptionType ->
|
||||
if (exceptionType.isNotEmpty()) {
|
||||
/**
|
||||
* This is a catch block; the wrapping function is allowed to throw exceptions.
|
||||
*/
|
||||
invokeStatic("sandbox/java/lang/DJVM", "catch", "(Ljava/lang/Throwable;)Lsandbox/java/lang/Throwable;")
|
||||
|
||||
/**
|
||||
* When catching exceptions, we also need to tell the verifier which
|
||||
* which kind of [sandbox.java.lang.Throwable] to expect this to be.
|
||||
*/
|
||||
castObjectTo(exceptionType)
|
||||
} else {
|
||||
/**
|
||||
* This is a finally block; the wrapping function MUST NOT throw exceptions.
|
||||
*/
|
||||
invokeStatic("sandbox/java/lang/DJVM", "finally", "(Ljava/lang/Throwable;)Lsandbox/java/lang/Throwable;")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val priority: Int
|
||||
get() = EMIT_HANDLING_EXCEPTIONS
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
import org.objectweb.asm.Opcodes.ATHROW
|
||||
|
||||
/**
|
||||
* Converts a [sandbox.java.lang.Throwable] into a [java.lang.Throwable]
|
||||
* so that the JVM can throw it.
|
||||
*/
|
||||
class ThrowExceptionWrapper : Emitter {
|
||||
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||
when (instruction.operation) {
|
||||
ATHROW -> {
|
||||
invokeStatic("sandbox/java/lang/DJVM", "fromDJVM", "(Lsandbox/java/lang/Throwable;)Ljava/lang/Throwable;")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package net.corda.djvm.rules.implementation.instrumentation
|
||||
|
||||
import net.corda.djvm.code.EMIT_TRACING
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
@ -40,7 +41,7 @@ class TraceAllocations : Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
override val isTracer: Boolean
|
||||
get() = true
|
||||
override val priority: Int
|
||||
get() = EMIT_TRACING
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.djvm.rules.implementation.instrumentation
|
||||
|
||||
import net.corda.djvm.code.EMIT_TRACING
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
@ -17,7 +18,7 @@ class TraceInvocations : Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
override val isTracer: Boolean
|
||||
get() = true
|
||||
override val priority: Int
|
||||
get() = EMIT_TRACING
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.djvm.rules.implementation.instrumentation
|
||||
|
||||
import net.corda.djvm.code.EMIT_TRACING
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
@ -17,7 +18,7 @@ class TraceJumps : Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
override val isTracer: Boolean
|
||||
get() = true
|
||||
override val priority: Int
|
||||
get() = EMIT_TRACING
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.djvm.rules.implementation.instrumentation
|
||||
|
||||
import net.corda.djvm.code.EMIT_TRACING
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
@ -17,7 +18,7 @@ class TraceThrows : Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
override val isTracer: Boolean
|
||||
get() = true
|
||||
override val priority: Int
|
||||
get() = EMIT_TRACING
|
||||
|
||||
}
|
||||
|
@ -21,12 +21,14 @@ class ClassSource private constructor(
|
||||
/**
|
||||
* Instantiate a [ClassSource] from a fully qualified class name.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromClassName(className: String, origin: String? = null) =
|
||||
ClassSource(className, origin)
|
||||
|
||||
/**
|
||||
* Instantiate a [ClassSource] from a file on disk.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromPath(path: Path) = PathClassSource(path)
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,8 @@ package net.corda.djvm.source
|
||||
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.analysis.ClassResolver
|
||||
import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner
|
||||
import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException
|
||||
import net.corda.djvm.analysis.SourceLocation
|
||||
import net.corda.djvm.code.asResourcePath
|
||||
import net.corda.djvm.messages.Message
|
||||
@ -61,7 +63,15 @@ abstract class AbstractSourceClassLoader(
|
||||
*/
|
||||
override fun loadClass(name: String, resolve: Boolean): Class<*> {
|
||||
logger.trace("Loading class {}, resolve={}...", name, resolve)
|
||||
val originalName = classResolver.reverseNormalized(name)
|
||||
val originalName = classResolver.reverseNormalized(name).let { n ->
|
||||
// A synthetic exception should be mapped back to its
|
||||
// corresponding exception in the original hierarchy.
|
||||
if (isDJVMException(n)) {
|
||||
getDJVMExceptionOwner(n)
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
return super.loadClass(originalName, resolve)
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,10 @@ import sandbox.java.lang.unsandbox
|
||||
|
||||
typealias SandboxFunction<TInput, TOutput> = sandbox.java.util.function.Function<TInput, TOutput>
|
||||
|
||||
@Suppress("unused")
|
||||
internal fun isEntryPoint(elt: java.lang.StackTraceElement): Boolean {
|
||||
return elt.className == "sandbox.Task" && elt.methodName == "apply"
|
||||
}
|
||||
|
||||
class Task(private val function: SandboxFunction<in Any?, out Any?>?) : SandboxFunction<Any?, Any?> {
|
||||
|
||||
/**
|
||||
|
@ -2,19 +2,25 @@
|
||||
@file:Suppress("unused")
|
||||
package sandbox.java.lang
|
||||
|
||||
import net.corda.djvm.analysis.AnalysisConfiguration.Companion.JVM_EXCEPTIONS
|
||||
import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMException
|
||||
import net.corda.djvm.rules.implementation.*
|
||||
import org.objectweb.asm.Opcodes.ACC_ENUM
|
||||
import org.objectweb.asm.Type
|
||||
import sandbox.isEntryPoint
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
|
||||
private const val SANDBOX_PREFIX = "sandbox."
|
||||
|
||||
fun Any.unsandbox(): Any {
|
||||
return when (this) {
|
||||
is Enum<*> -> fromDJVMEnum()
|
||||
is Object -> fromDJVM()
|
||||
is Array<*> -> fromDJVMArray()
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ClassNotFoundException::class)
|
||||
fun Any.sandbox(): Any {
|
||||
return when (this) {
|
||||
is kotlin.String -> String.toDJVM(this)
|
||||
@ -27,6 +33,7 @@ fun Any.sandbox(): Any {
|
||||
is kotlin.Double -> Double.toDJVM(this)
|
||||
is kotlin.Boolean -> Boolean.toDJVM(this)
|
||||
is kotlin.Enum<*> -> toDJVMEnum()
|
||||
is kotlin.Throwable -> toDJVMThrowable()
|
||||
is Array<*> -> toDJVMArray<Object>()
|
||||
else -> this
|
||||
}
|
||||
@ -38,8 +45,11 @@ private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this)
|
||||
* These functions use the "current" classloader, i.e. classloader
|
||||
* that owns this DJVM class.
|
||||
*/
|
||||
private fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage())
|
||||
private fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage())
|
||||
@Throws(ClassNotFoundException::class)
|
||||
internal fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage())
|
||||
|
||||
@Throws(ClassNotFoundException::class)
|
||||
internal fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage())
|
||||
|
||||
private fun kotlin.String.toSandboxPackage(): kotlin.String {
|
||||
return if (startsWith(SANDBOX_PREFIX)) {
|
||||
@ -66,10 +76,12 @@ private inline fun <reified T : Object> Array<*>.toDJVMArray(): Array<out T?> {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> {
|
||||
@Throws(ClassNotFoundException::class)
|
||||
internal fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> {
|
||||
return javaClass.fromDJVMType().enumConstants[ordinal()] as kotlin.Enum<*>
|
||||
}
|
||||
|
||||
@Throws(ClassNotFoundException::class)
|
||||
private fun kotlin.Enum<*>.toDJVMEnum(): Enum<*> {
|
||||
@Suppress("unchecked_cast")
|
||||
return (getEnumConstants(javaClass.toDJVMType() as Class<Enum<*>>) as Array<Enum<*>>)[ordinal]
|
||||
@ -87,10 +99,11 @@ fun getEnumConstants(clazz: Class<out Enum<*>>): Array<*>? {
|
||||
|
||||
internal fun enumConstantDirectory(clazz: Class<out Enum<*>>): sandbox.java.util.Map<String, out Enum<*>>? {
|
||||
// DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map.
|
||||
@Suppress("ReplaceGetOrSet")
|
||||
return allEnumDirectories.get(clazz) ?: createEnumDirectory(clazz)
|
||||
}
|
||||
|
||||
@Suppress("unchecked_cast")
|
||||
@Suppress("unchecked_cast", "ReplaceGetOrSet")
|
||||
internal fun getEnumConstantsShared(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
|
||||
return if (isEnum(clazz)) {
|
||||
// DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map.
|
||||
@ -100,7 +113,7 @@ internal fun getEnumConstantsShared(clazz: Class<out Enum<*>>): Array<out Enum<*
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unchecked_cast")
|
||||
@Suppress("unchecked_cast", "ReplacePutWithAssignment" )
|
||||
private fun createEnum(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
|
||||
return clazz.getMethod("values").let { method ->
|
||||
method.isAccessible = true
|
||||
@ -109,6 +122,7 @@ private fun createEnum(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
|
||||
}?.apply { allEnums.put(clazz, this) }
|
||||
}
|
||||
|
||||
@Suppress("ReplacePutWithAssignment")
|
||||
private fun createEnumDirectory(clazz: Class<out Enum<*>>): sandbox.java.util.Map<String, out Enum<*>> {
|
||||
val universe = getEnumConstantsShared(clazz) ?: throw IllegalArgumentException("${clazz.name} is not an enum type")
|
||||
val directory = sandbox.java.util.LinkedHashMap<String, Enum<*>>(2 * universe.size)
|
||||
@ -154,5 +168,130 @@ private fun toSandbox(className: kotlin.String): kotlin.String {
|
||||
private val bannedClasses = setOf(
|
||||
"^java\\.lang\\.DJVM(.*)?\$".toRegex(),
|
||||
"^net\\.corda\\.djvm\\..*\$".toRegex(),
|
||||
"^Task\$".toRegex()
|
||||
"^Task(.*)?\$".toRegex()
|
||||
)
|
||||
|
||||
/**
|
||||
* Exception Management.
|
||||
*
|
||||
* This function converts a [sandbox.java.lang.Throwable] into a
|
||||
* [java.lang.Throwable] that the JVM can actually throw.
|
||||
*/
|
||||
fun fromDJVM(t: Throwable?): kotlin.Throwable {
|
||||
return if (t is DJVMThrowableWrapper) {
|
||||
// We must be exiting a finally block.
|
||||
t.fromDJVM()
|
||||
} else {
|
||||
try {
|
||||
/**
|
||||
* Someone has created a [sandbox.java.lang.Throwable]
|
||||
* and is (re?)throwing it.
|
||||
*/
|
||||
val sandboxedName = t!!.javaClass.name
|
||||
if (Type.getInternalName(t.javaClass) in JVM_EXCEPTIONS) {
|
||||
// We map these exceptions to their equivalent JVM classes.
|
||||
Class.forName(sandboxedName.fromSandboxPackage()).createJavaThrowable(t)
|
||||
} else {
|
||||
// Whereas the sandbox creates a synthetic throwable wrapper for these.
|
||||
Class.forName(getDJVMException(sandboxedName))
|
||||
.getDeclaredConstructor(sandboxThrowable)
|
||||
.newInstance(t) as kotlin.Throwable
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
RuleViolationError(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a [java.lang.Throwable] inside a [sandbox.java.lang.Throwable].
|
||||
* This function is invoked at the beginning of a finally block, and
|
||||
* so does not need to return a reference to the equivalent sandboxed
|
||||
* exception. The finally block only needs to be able to re-throw the
|
||||
* original exception when it finishes.
|
||||
*/
|
||||
fun finally(t: kotlin.Throwable): Throwable = DJVMThrowableWrapper(t)
|
||||
|
||||
/**
|
||||
* Converts a [java.lang.Throwable] into a [sandbox.java.lang.Throwable].
|
||||
* It is invoked at the start of each catch block.
|
||||
*
|
||||
* Note: [DisallowCatchingBlacklistedExceptions] means that we don't
|
||||
* need to handle [ThreadDeath] here.
|
||||
*/
|
||||
fun catch(t: kotlin.Throwable): Throwable {
|
||||
try {
|
||||
return t.toDJVMThrowable()
|
||||
} catch (e: Exception) {
|
||||
throw RuleViolationError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker functions to convert [java.lang.Throwable] into [sandbox.java.lang.Throwable].
|
||||
*/
|
||||
private fun kotlin.Throwable.toDJVMThrowable(): Throwable {
|
||||
return (this as? DJVMException)?.getThrowable() ?: javaClass.toDJVMType().createDJVMThrowable(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new [sandbox.java.lang.Throwable] from a [java.lang.Throwable],
|
||||
* which was probably thrown by the JVM itself.
|
||||
*/
|
||||
private fun Class<*>.createDJVMThrowable(t: kotlin.Throwable): Throwable {
|
||||
return (try {
|
||||
getDeclaredConstructor(String::class.java).newInstance(String.toDJVM(t.message))
|
||||
} catch (e: NoSuchMethodException) {
|
||||
newInstance()
|
||||
} as Throwable).apply {
|
||||
t.cause?.also {
|
||||
initCause(it.toDJVMThrowable())
|
||||
}
|
||||
stackTrace = sanitiseToDJVM(t.stackTrace)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Class<*>.createJavaThrowable(t: Throwable): kotlin.Throwable {
|
||||
return (try {
|
||||
getDeclaredConstructor(kotlin.String::class.java).newInstance(String.fromDJVM(t.message))
|
||||
} catch (e: NoSuchMethodException) {
|
||||
newInstance()
|
||||
} as kotlin.Throwable).apply {
|
||||
t.cause?.also {
|
||||
initCause(fromDJVM(it))
|
||||
}
|
||||
stackTrace = copyFromDJVM(t.stackTrace)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitiseToDJVM(source: Array<java.lang.StackTraceElement>): Array<StackTraceElement> {
|
||||
var idx = 0
|
||||
while (idx < source.size && !isEntryPoint(source[idx])) {
|
||||
++idx
|
||||
}
|
||||
return copyToDJVM(source, 0, idx)
|
||||
}
|
||||
|
||||
internal fun copyToDJVM(source: Array<java.lang.StackTraceElement>, fromIdx: Int, toIdx: Int): Array<StackTraceElement> {
|
||||
return source.sliceArray(fromIdx until toIdx).map(::toDJVM).toTypedArray()
|
||||
}
|
||||
|
||||
private fun toDJVM(elt: java.lang.StackTraceElement) = StackTraceElement(
|
||||
String.toDJVM(elt.className),
|
||||
String.toDJVM(elt.methodName),
|
||||
String.toDJVM(elt.fileName),
|
||||
elt.lineNumber
|
||||
)
|
||||
|
||||
private fun copyFromDJVM(source: Array<StackTraceElement>): Array<java.lang.StackTraceElement> {
|
||||
return source.map(::fromDJVM).toTypedArray()
|
||||
}
|
||||
|
||||
private fun fromDJVM(elt: StackTraceElement) = java.lang.StackTraceElement(
|
||||
String.fromDJVM(elt.className),
|
||||
String.fromDJVM(elt.methodName),
|
||||
String.fromDJVM(elt.fileName),
|
||||
elt.lineNumber
|
||||
)
|
||||
|
||||
private val sandboxThrowable: Class<*> = Throwable::class.java
|
||||
|
12
djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt
Normal file
12
djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package sandbox.java.lang
|
||||
|
||||
/**
|
||||
* All synthetic [Throwable] classes wrapping non-JVM exceptions
|
||||
* will implement this interface.
|
||||
*/
|
||||
interface DJVMException {
|
||||
/**
|
||||
* Returns the [sandbox.java.lang.Throwable] instance inside the wrapper.
|
||||
*/
|
||||
fun getThrowable(): Throwable
|
||||
}
|
@ -6,4 +6,4 @@ package sandbox.net.corda.djvm.costing
|
||||
*
|
||||
* @property message The description of the condition causing the problem.
|
||||
*/
|
||||
class ThresholdViolationError(override val message: String) : ThreadDeath()
|
||||
class ThresholdViolationError(override val message: String?) : ThreadDeath()
|
||||
|
@ -7,4 +7,4 @@ package sandbox.net.corda.djvm.rules
|
||||
*
|
||||
* @property message The description of the condition causing the problem.
|
||||
*/
|
||||
class RuleViolationError(override val message: String) : ThreadDeath()
|
||||
class RuleViolationError(override val message: String?) : ThreadDeath()
|
24
djvm/src/test/java/net/corda/djvm/WithJava.java
Normal file
24
djvm/src/test/java/net/corda/djvm/WithJava.java
Normal file
@ -0,0 +1,24 @@
|
||||
package net.corda.djvm;
|
||||
|
||||
import net.corda.djvm.execution.ExecutionSummaryWithResult;
|
||||
import net.corda.djvm.execution.SandboxExecutor;
|
||||
import net.corda.djvm.source.ClassSource;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public interface WithJava {
|
||||
|
||||
static <T,R> ExecutionSummaryWithResult<R> run(
|
||||
SandboxExecutor<T, R> executor, Class<? extends Function<T,R>> task, T input) {
|
||||
try {
|
||||
return executor.run(ClassSource.fromClassName(task.getName(), null), input);
|
||||
} catch (Exception e) {
|
||||
if (e instanceof RuntimeException) {
|
||||
throw (RuntimeException) e;
|
||||
} else {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package net.corda.djvm.execution;
|
||||
|
||||
import net.corda.djvm.TestBase;
|
||||
import net.corda.djvm.WithJava;
|
||||
import static net.corda.djvm.messages.Severity.*;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
public class SandboxEnumJavaTest extends TestBase {
|
||||
|
||||
@Test
|
||||
public void testEnumInsideSandbox() {
|
||||
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
|
||||
SandboxExecutor<Integer, String[]> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<String[]> output = WithJava.run(executor, TransformEnum.class, 0);
|
||||
assertThat(output.getResult())
|
||||
.isEqualTo(new String[]{ "ONE", "TWO", "THREE" });
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnEnumFromSandbox() {
|
||||
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
|
||||
SandboxExecutor<String, ExampleEnum> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<ExampleEnum> output = WithJava.run(executor, FetchEnum.class, "THREE");
|
||||
assertThat(output.getResult())
|
||||
.isEqualTo(ExampleEnum.THREE);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWeCanIdentifyClassAsEnum() {
|
||||
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
|
||||
SandboxExecutor<ExampleEnum, Boolean> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<Boolean> output = WithJava.run(executor, AssertEnum.class, ExampleEnum.THREE);
|
||||
assertThat(output.getResult()).isTrue();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWeCanCreateEnumMap() {
|
||||
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
|
||||
SandboxExecutor<ExampleEnum, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<Integer> output = WithJava.run(executor, UseEnumMap.class, ExampleEnum.TWO);
|
||||
assertThat(output.getResult()).isEqualTo(1);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWeCanCreateEnumSet() {
|
||||
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
|
||||
SandboxExecutor<ExampleEnum, Boolean> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<Boolean> output = WithJava.run(executor, UseEnumSet.class, ExampleEnum.ONE);
|
||||
assertThat(output.getResult()).isTrue();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static class AssertEnum implements Function<ExampleEnum, Boolean> {
|
||||
@Override
|
||||
public Boolean apply(ExampleEnum input) {
|
||||
return input.getClass().isEnum();
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransformEnum implements Function<Integer, String[]> {
|
||||
@Override
|
||||
public String[] apply(Integer input) {
|
||||
return Stream.of(ExampleEnum.values()).map(ExampleEnum::name).toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
public static class FetchEnum implements Function<String, ExampleEnum> {
|
||||
public ExampleEnum apply(String input) {
|
||||
return ExampleEnum.valueOf(input);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UseEnumMap implements Function<ExampleEnum, Integer> {
|
||||
@Override
|
||||
public Integer apply(ExampleEnum input) {
|
||||
Map<ExampleEnum, String> map = new EnumMap<>(ExampleEnum.class);
|
||||
map.put(input, input.name());
|
||||
return map.size();
|
||||
}
|
||||
}
|
||||
|
||||
public static class UseEnumSet implements Function<ExampleEnum, Boolean> {
|
||||
@Override
|
||||
public Boolean apply(ExampleEnum input) {
|
||||
return EnumSet.allOf(ExampleEnum.class).contains(input);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package net.corda.djvm.execution;
|
||||
|
||||
import net.corda.djvm.TestBase;
|
||||
import net.corda.djvm.WithJava;
|
||||
import static net.corda.djvm.messages.Severity.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
public class SandboxThrowableJavaTest extends TestBase {
|
||||
|
||||
@Test
|
||||
public void testUserExceptionHandling() {
|
||||
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
|
||||
SandboxExecutor<String, String[]> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<String[]> output = WithJava.run(executor, ThrowAndCatchJavaExample.class, "Hello World!");
|
||||
assertThat(output.getResult())
|
||||
.isEqualTo(new String[]{ "FIRST FINALLY", "BASE EXCEPTION", "Hello World!", "SECOND FINALLY" });
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckedExceptions() {
|
||||
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
|
||||
SandboxExecutor<String, String> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
|
||||
ExecutionSummaryWithResult<String> success = WithJava.run(executor, JavaWithCheckedExceptions.class, "http://localhost:8080/hello/world");
|
||||
assertThat(success.getResult()).isEqualTo("/hello/world");
|
||||
|
||||
ExecutionSummaryWithResult<String> failure = WithJava.run(executor, JavaWithCheckedExceptions.class, "nasty string");
|
||||
assertThat(failure.getResult()).isEqualTo("CATCH:Illegal character in path at index 5: nasty string");
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static class ThrowAndCatchJavaExample implements Function<String, String[]> {
|
||||
@Override
|
||||
public String[] apply(String input) {
|
||||
List<String> data = new LinkedList<>();
|
||||
try {
|
||||
try {
|
||||
throw new MyExampleException(input);
|
||||
} finally {
|
||||
data.add("FIRST FINALLY");
|
||||
}
|
||||
} catch (MyBaseException e) {
|
||||
data.add("BASE EXCEPTION");
|
||||
data.add(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
data.add("NOT THIS ONE!");
|
||||
} finally {
|
||||
data.add("SECOND FINALLY");
|
||||
}
|
||||
|
||||
return data.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public static class JavaWithCheckedExceptions implements Function<String, String> {
|
||||
@Override
|
||||
public String apply(String input) {
|
||||
try {
|
||||
return new URI(input).getPath();
|
||||
} catch (URISyntaxException e) {
|
||||
return "CATCH:" + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt
Normal file
100
djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt
Normal file
@ -0,0 +1,100 @@
|
||||
package net.corda.djvm
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
import sandbox.SandboxFunction
|
||||
import sandbox.Task
|
||||
import sandbox.java.lang.sandbox
|
||||
|
||||
class DJVMExceptionTest {
|
||||
@Test
|
||||
fun testSingleException() {
|
||||
val result = Task(SingleExceptionTask()).apply("Hello World")
|
||||
assertThat(result).isInstanceOf(Throwable::class.java)
|
||||
result as Throwable
|
||||
|
||||
assertThat(result.message).isEqualTo("Hello World")
|
||||
assertThat(result.cause).isNull()
|
||||
assertThat(result.stackTrace)
|
||||
.hasSize(2)
|
||||
.allSatisfy { it is StackTraceElement && it.className == result.javaClass.name }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleExceptions() {
|
||||
val result = Task(MultipleExceptionsTask()).apply("Hello World")
|
||||
assertThat(result).isInstanceOf(Throwable::class.java)
|
||||
result as Throwable
|
||||
|
||||
assertThat(result.message).isEqualTo("Hello World(1)(2)")
|
||||
assertThat(result.cause).isInstanceOf(Throwable::class.java)
|
||||
assertThat(result.stackTrace)
|
||||
.hasSize(2)
|
||||
.allSatisfy { it is StackTraceElement && it.className == result.javaClass.name }
|
||||
val resultLineNumbers = result.stackTrace.toLineNumbers()
|
||||
|
||||
val firstCause = result.cause as Throwable
|
||||
assertThat(firstCause.message).isEqualTo("Hello World(1)")
|
||||
assertThat(firstCause.cause).isInstanceOf(Throwable::class.java)
|
||||
assertThat(firstCause.stackTrace)
|
||||
.hasSize(2)
|
||||
.allSatisfy { it is StackTraceElement && it.className == result.javaClass.name }
|
||||
val firstCauseLineNumbers = firstCause.stackTrace.toLineNumbers()
|
||||
|
||||
val rootCause = firstCause.cause as Throwable
|
||||
assertThat(rootCause.message).isEqualTo("Hello World")
|
||||
assertThat(rootCause.cause).isNull()
|
||||
assertThat(rootCause.stackTrace)
|
||||
.hasSize(2)
|
||||
.allSatisfy { it is StackTraceElement && it.className == result.javaClass.name }
|
||||
val rootCauseLineNumbers = rootCause.stackTrace.toLineNumbers()
|
||||
|
||||
// These stack traces should share one line number and have one distinct line number each.
|
||||
assertThat(resultLineNumbers.toSet() + firstCauseLineNumbers.toSet() + rootCauseLineNumbers.toSet())
|
||||
.hasSize(4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJavaThrowableToSandbox() {
|
||||
val result = Throwable("Hello World").sandbox()
|
||||
assertThat(result).isInstanceOf(sandbox.java.lang.Throwable::class.java)
|
||||
result as sandbox.java.lang.Throwable
|
||||
|
||||
assertThat(result.message).isEqualTo("Hello World".toDJVM())
|
||||
assertThat(result.stackTrace).isNotEmpty()
|
||||
assertThat(result.cause).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWeTryToCreateCorrectSandboxExceptionsAtRuntime() {
|
||||
assertThatExceptionOfType(ClassNotFoundException::class.java)
|
||||
.isThrownBy { Exception("Hello World").sandbox() }
|
||||
.withMessage("sandbox.java.lang.Exception")
|
||||
assertThatExceptionOfType(ClassNotFoundException::class.java)
|
||||
.isThrownBy { RuntimeException("Hello World").sandbox() }
|
||||
.withMessage("sandbox.java.lang.RuntimeException")
|
||||
}
|
||||
}
|
||||
|
||||
class SingleExceptionTask : SandboxFunction<Any?, sandbox.java.lang.Throwable> {
|
||||
override fun apply(input: Any?): sandbox.java.lang.Throwable? {
|
||||
return sandbox.java.lang.Throwable(input as? sandbox.java.lang.String)
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleExceptionsTask : SandboxFunction<Any?, sandbox.java.lang.Throwable> {
|
||||
override fun apply(input: Any?): sandbox.java.lang.Throwable? {
|
||||
val root = sandbox.java.lang.Throwable(input as? sandbox.java.lang.String)
|
||||
val nested = sandbox.java.lang.Throwable(root.message + "(1)", root)
|
||||
return sandbox.java.lang.Throwable(nested.message + "(2)", nested)
|
||||
}
|
||||
}
|
||||
|
||||
private infix operator fun sandbox.java.lang.String.plus(s: String): sandbox.java.lang.String {
|
||||
return (toString() + s).toDJVM()
|
||||
}
|
||||
|
||||
private fun Array<StackTraceElement>.toLineNumbers(): IntArray {
|
||||
return map(StackTraceElement::getLineNumber).toIntArray()
|
||||
}
|
@ -113,14 +113,4 @@ class DJVMTest {
|
||||
assertArrayEquals(ByteArray(1) { 127.toByte() }, result[9] as ByteArray)
|
||||
assertArrayEquals(CharArray(1) { '?' }, result[10] as CharArray)
|
||||
}
|
||||
|
||||
private fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this)
|
||||
private fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this)
|
||||
private fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this)
|
||||
private fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this)
|
||||
private fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this)
|
||||
private fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this)
|
||||
private fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this)
|
||||
private fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this)
|
||||
private fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this)
|
||||
}
|
@ -37,22 +37,29 @@ abstract class TestBase {
|
||||
val ALL_EMITTERS = Discovery.find<Emitter>()
|
||||
|
||||
// We need at least these emitters to handle the Java API classes.
|
||||
@JvmField
|
||||
val BASIC_EMITTERS: List<Emitter> = listOf(
|
||||
ArgumentUnwrapper(),
|
||||
HandleExceptionUnwrapper(),
|
||||
ReturnTypeWrapper(),
|
||||
RewriteClassMethods(),
|
||||
StringConstantWrapper()
|
||||
StringConstantWrapper(),
|
||||
ThrowExceptionWrapper()
|
||||
)
|
||||
|
||||
val ALL_DEFINITION_PROVIDERS = Discovery.find<DefinitionProvider>()
|
||||
|
||||
// We need at least these providers to handle the Java API classes.
|
||||
@JvmField
|
||||
val BASIC_DEFINITION_PROVIDERS: List<DefinitionProvider> = listOf(StaticConstantRemover())
|
||||
|
||||
@JvmField
|
||||
val BLANK = emptySet<Any>()
|
||||
|
||||
@JvmField
|
||||
val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass)
|
||||
|
||||
@JvmField
|
||||
val DETERMINISTIC_RT: Path = Paths.get(
|
||||
System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set"))
|
||||
|
||||
@ -89,7 +96,7 @@ abstract class TestBase {
|
||||
val reader = ClassReader(T::class.java.name)
|
||||
AnalysisConfiguration(
|
||||
minimumSeverityLevel = minimumSeverityLevel,
|
||||
classPath = listOf(DETERMINISTIC_RT)
|
||||
bootstrapJar = DETERMINISTIC_RT
|
||||
).use { analysisConfiguration ->
|
||||
val validator = RuleValidator(ALL_RULES, analysisConfiguration)
|
||||
val context = AnalysisContext.fromConfiguration(analysisConfiguration)
|
||||
|
@ -1,22 +1,24 @@
|
||||
@file:JvmName("UtilityFunctions")
|
||||
package net.corda.djvm
|
||||
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
|
||||
/**
|
||||
* Allows us to create a [Utilities] object that we can pin inside the sandbox.
|
||||
*/
|
||||
object Utilities {
|
||||
fun throwRuleViolationError(): Nothing = throw RuleViolationError("Can't catch this!")
|
||||
|
||||
fun throwThresholdViolationError(): Nothing = throw ThresholdViolationError("Can't catch this!")
|
||||
|
||||
fun throwContractConstraintViolation(): Nothing = throw IllegalArgumentException("Contract constraint violated")
|
||||
|
||||
fun throwError(): Nothing = throw Error()
|
||||
|
||||
fun throwThrowable(): Nothing = throw Throwable()
|
||||
|
||||
fun throwThreadDeath(): Nothing = throw ThreadDeath()
|
||||
|
||||
fun throwStackOverflowError(): Nothing = throw StackOverflowError("FAKE OVERFLOW!")
|
||||
|
||||
fun throwOutOfMemoryError(): Nothing = throw OutOfMemoryError("FAKE OOM!")
|
||||
}
|
||||
|
||||
fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this)
|
||||
fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this)
|
||||
fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this)
|
||||
fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this)
|
||||
fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this)
|
||||
fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this)
|
||||
fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this)
|
||||
fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this)
|
||||
fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this)
|
@ -6,14 +6,8 @@ import foo.bar.sandbox.toNumber
|
||||
import net.corda.djvm.TestBase
|
||||
import net.corda.djvm.analysis.Whitelist
|
||||
import net.corda.djvm.Utilities
|
||||
import net.corda.djvm.Utilities.throwContractConstraintViolation
|
||||
import net.corda.djvm.Utilities.throwError
|
||||
import net.corda.djvm.Utilities.throwOutOfMemoryError
|
||||
import net.corda.djvm.Utilities.throwRuleViolationError
|
||||
import net.corda.djvm.Utilities.throwStackOverflowError
|
||||
import net.corda.djvm.Utilities.throwThreadDeath
|
||||
import net.corda.djvm.Utilities.throwThresholdViolationError
|
||||
import net.corda.djvm.Utilities.throwThrowable
|
||||
import net.corda.djvm.assertions.AssertionExtensions.withProblem
|
||||
import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -55,7 +49,7 @@ class SandboxExecutorTest : TestBase() {
|
||||
|
||||
class Contract : Function<Transaction, Unit> {
|
||||
override fun apply(input: Transaction) {
|
||||
throwContractConstraintViolation()
|
||||
throw IllegalArgumentException("Contract constraint violated")
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,11 +68,7 @@ class SandboxExecutorTest : TestBase() {
|
||||
val obj = Object()
|
||||
val hash1 = obj.hashCode()
|
||||
val hash2 = obj.hashCode()
|
||||
//require(hash1 == hash2)
|
||||
// TODO: Replace require() once we have working exception support.
|
||||
if (hash1 != hash2) {
|
||||
throwError()
|
||||
}
|
||||
require(hash1 == hash2)
|
||||
return Object().hashCode()
|
||||
}
|
||||
}
|
||||
@ -180,7 +170,7 @@ class SandboxExecutorTest : TestBase() {
|
||||
class TestCatchThreadDeath : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
throwThreadDeath()
|
||||
throw ThreadDeath()
|
||||
} catch (exception: ThreadDeath) {
|
||||
1
|
||||
}
|
||||
@ -261,8 +251,8 @@ class SandboxExecutorTest : TestBase() {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
when (input) {
|
||||
1 -> throwThrowable()
|
||||
2 -> throwError()
|
||||
1 -> throw Throwable()
|
||||
2 -> throw Error()
|
||||
else -> 0
|
||||
}
|
||||
} catch (exception: Error) {
|
||||
@ -277,20 +267,20 @@ class SandboxExecutorTest : TestBase() {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
when (input) {
|
||||
1 -> throwThrowable()
|
||||
2 -> throwError()
|
||||
1 -> throw Throwable()
|
||||
2 -> throw Error()
|
||||
3 -> try {
|
||||
throwThreadDeath()
|
||||
throw ThreadDeath()
|
||||
} catch (ex: ThreadDeath) {
|
||||
3
|
||||
}
|
||||
4 -> try {
|
||||
throwStackOverflowError()
|
||||
throw StackOverflowError("FAKE OVERFLOW!")
|
||||
} catch (ex: StackOverflowError) {
|
||||
4
|
||||
}
|
||||
5 -> try {
|
||||
throwOutOfMemoryError()
|
||||
throw OutOfMemoryError("FAKE OOM!")
|
||||
} catch (ex: OutOfMemoryError) {
|
||||
5
|
||||
}
|
||||
|
@ -0,0 +1,95 @@
|
||||
package net.corda.djvm.execution
|
||||
|
||||
import net.corda.djvm.TestBase
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Test
|
||||
import java.util.function.Function
|
||||
|
||||
class SandboxThrowableTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `test user exception handling`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<String, Array<String>>(configuration)
|
||||
contractExecutor.run<ThrowAndCatchExample>("Hello World").apply {
|
||||
assertThat(result)
|
||||
.isEqualTo(arrayOf("FIRST FINALLY", "BASE EXCEPTION", "Hello World", "SECOND FINALLY"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test rethrowing an exception`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<String, Array<String>>(configuration)
|
||||
contractExecutor.run<ThrowAndRethrowExample>("Hello World").apply {
|
||||
assertThat(result)
|
||||
.isEqualTo(arrayOf("FIRST CATCH", "FIRST FINALLY", "SECOND CATCH", "Hello World", "SECOND FINALLY"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test JVM exceptions still propagate`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
|
||||
contractExecutor.run<TriggerJVMException>(-1).apply {
|
||||
assertThat(result)
|
||||
.isEqualTo("sandbox.java.lang.ArrayIndexOutOfBoundsException:-1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ThrowAndRethrowExample : Function<String, Array<String>> {
|
||||
override fun apply(input: String): Array<String> {
|
||||
val data = mutableListOf<String>()
|
||||
try {
|
||||
try {
|
||||
throw MyExampleException(input)
|
||||
} catch (e: Exception) {
|
||||
data += "FIRST CATCH"
|
||||
throw e
|
||||
} finally {
|
||||
data += "FIRST FINALLY"
|
||||
}
|
||||
} catch (e: MyExampleException) {
|
||||
data += "SECOND CATCH"
|
||||
e.message?.apply { data += this }
|
||||
} finally {
|
||||
data += "SECOND FINALLY"
|
||||
}
|
||||
|
||||
return data.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
class ThrowAndCatchExample : Function<String, Array<String>> {
|
||||
override fun apply(input: String): Array<String> {
|
||||
val data = mutableListOf<String>()
|
||||
try {
|
||||
try {
|
||||
throw MyExampleException(input)
|
||||
} finally {
|
||||
data += "FIRST FINALLY"
|
||||
}
|
||||
} catch (e: MyBaseException) {
|
||||
data += "BASE EXCEPTION"
|
||||
e.message?.apply { data += this }
|
||||
} catch (e: Exception) {
|
||||
data += "NOT THIS ONE!"
|
||||
} finally {
|
||||
data += "SECOND FINALLY"
|
||||
}
|
||||
|
||||
return data.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
class TriggerJVMException : Function<Int, String> {
|
||||
override fun apply(input: Int): String {
|
||||
return try {
|
||||
arrayOf(0, 1, 2)[input]
|
||||
"No Error"
|
||||
} catch (e: Exception) {
|
||||
e.javaClass.name + ':' + (e.message ?: "<MESSAGE MISSING>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class MyBaseException(message: String) : Exception(message)
|
||||
class MyExampleException(message: String) : MyBaseException(message)
|
Loading…
x
Reference in New Issue
Block a user