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:
Chris Rankin 2018-10-19 17:23:14 +01:00 committed by GitHub
parent e62a3edcd1
commit e10119031c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1508 additions and 192 deletions

View File

@ -33,7 +33,6 @@ dependencies {
// ASM: byte code manipulation library // ASM: byte code manipulation library
compile "org.ow2.asm:asm:$asm_version" compile "org.ow2.asm:asm:$asm_version"
compile "org.ow2.asm:asm-tree:$asm_version"
compile "org.ow2.asm:asm-commons:$asm_version" compile "org.ow2.asm:asm-commons:$asm_version"
// ClassGraph: classpath scanning // ClassGraph: classpath scanning
@ -62,6 +61,7 @@ shadowJar {
exclude 'sandbox/java/lang/Comparable.class' exclude 'sandbox/java/lang/Comparable.class'
exclude 'sandbox/java/lang/Enum.class' exclude 'sandbox/java/lang/Enum.class'
exclude 'sandbox/java/lang/Iterable.class' exclude 'sandbox/java/lang/Iterable.class'
exclude 'sandbox/java/lang/StackTraceElement.class'
exclude 'sandbox/java/lang/StringBuffer.class' exclude 'sandbox/java/lang/StringBuffer.class'
exclude 'sandbox/java/lang/StringBuilder.class' exclude 'sandbox/java/lang/StringBuilder.class'
exclude 'sandbox/java/nio/**' exclude 'sandbox/java/nio/**'

View File

@ -3,10 +3,10 @@ package sandbox.java.lang;
import java.io.IOException; import java.io.IOException;
/** /**
* This is a dummy class that implements just enough of [java.lang.Appendable] * This is a dummy class that implements just enough of {@link java.lang.Appendable}
* to keep [sandbox.java.lang.StringBuilder], [sandbox.java.lang.StringBuffer] * to keep {@link sandbox.java.lang.StringBuilder}, {@link sandbox.java.lang.StringBuffer}
* and [sandbox.java.lang.String] honest. * and {@link sandbox.java.lang.String} honest.
* Note that it does not extend [java.lang.Appendable]. * Note that it does not extend {@link java.lang.Appendable}.
*/ */
public interface Appendable { public interface Appendable {

View File

@ -3,8 +3,8 @@ package sandbox.java.lang;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* This is a dummy class that implements just enough of [java.lang.CharSequence] * This is a dummy class that implements just enough of {@link java.lang.CharSequence}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
public interface CharSequence extends java.lang.CharSequence { public interface CharSequence extends java.lang.CharSequence {

View File

@ -1,8 +1,8 @@
package sandbox.java.lang; package sandbox.java.lang;
/** /**
* This is a dummy class that implements just enough of [java.lang.Comparable] * This is a dummy class that implements just enough of {@link java.lang.Comparable}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
public interface Comparable<T> extends java.lang.Comparable<T> { public interface Comparable<T> extends java.lang.Comparable<T> {
} }

View File

@ -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;
}
}

View File

@ -1,11 +1,13 @@
package sandbox.java.lang; package sandbox.java.lang;
import org.jetbrains.annotations.NotNull;
import java.io.Serializable; import java.io.Serializable;
/** /**
* This is a dummy class. We will load the actual Enum class at run-time. * 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 { public abstract class Enum<E extends Enum<E>> extends Object implements Comparable<E>, Serializable {
private final String name; private final String name;
@ -24,4 +26,10 @@ public abstract class Enum<E extends Enum<E>> extends Object implements Comparab
return ordinal; return ordinal;
} }
@Override
@NotNull
final java.lang.Enum<?> fromDJVM() {
throw new UnsupportedOperationException("Dummy implementation");
}
} }

View File

@ -5,8 +5,8 @@ import org.jetbrains.annotations.NotNull;
import java.util.Iterator; import java.util.Iterator;
/** /**
* This is a dummy class that implements just enough of [java.lang.Iterable] * This is a dummy class that implements just enough of {@link java.lang.Iterable}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
public interface Iterable<T> extends java.lang.Iterable<T> { public interface Iterable<T> extends java.lang.Iterable<T> {
@Override @Override

View File

@ -54,8 +54,7 @@ public class Object {
private static Class<?> fromDJVM(Class<?> type) { private static Class<?> fromDJVM(Class<?> type) {
try { try {
java.lang.String name = type.getName(); return DJVM.fromDJVMType(type);
return Class.forName(name.startsWith("sandbox.") ? name.substring(8) : name);
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
throw new RuleViolationError(e.getMessage()); throw new RuleViolationError(e.getMessage());
} }

View 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 + ')' : "")
);
}
}

View File

@ -7,6 +7,8 @@ import sandbox.java.util.Locale;
import java.io.Serializable; import java.io.Serializable;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.util.Map;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class String extends Object implements Comparable<String>, CharSequence, Serializable { 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 TRUE = new String("true");
private static final String FALSE = new String("false"); 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; private final java.lang.String value;
public String() { public String() {
@ -88,6 +102,17 @@ public final class String extends Object implements Comparable<String>, CharSequ
this.value = builder.toString(); 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 @Override
public char charAt(int index) { public char charAt(int index) {
return value.charAt(index); return value.charAt(index);
@ -310,6 +335,8 @@ public final class String extends Object implements Comparable<String>, CharSequ
return toDJVM(value.trim()); return toDJVM(value.trim());
} }
public String intern() { return INTERNAL.computeIfAbsent(value, s -> this); }
public char[] toCharArray() { public char[] toCharArray() {
return value.toCharArray(); return value.toCharArray();
} }

View File

@ -3,8 +3,8 @@ package sandbox.java.lang;
import java.io.Serializable; import java.io.Serializable;
/** /**
* This is a dummy class that implements just enough of [java.lang.StringBuffer] * This is a dummy class that implements just enough of {@link java.lang.StringBuffer}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
public abstract class StringBuffer extends Object implements CharSequence, Appendable, Serializable { public abstract class StringBuffer extends Object implements CharSequence, Appendable, Serializable {

View File

@ -3,8 +3,8 @@ package sandbox.java.lang;
import java.io.Serializable; import java.io.Serializable;
/** /**
* This is a dummy class that implements just enough of [java.lang.StringBuilder] * This is a dummy class that implements just enough of {@link java.lang.StringBuilder}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
public abstract class StringBuilder extends Object implements Appendable, CharSequence, Serializable { public abstract class StringBuilder extends Object implements Appendable, CharSequence, Serializable {

View 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);
}
}

View File

@ -1,8 +1,8 @@
package sandbox.java.nio.charset; package sandbox.java.nio.charset;
/** /**
* This is a dummy class that implements just enough of [java.nio.charset.Charset] * This is a dummy class that implements just enough of {@link java.nio.charset.Charset}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public abstract class Charset extends sandbox.java.lang.Object { public abstract class Charset extends sandbox.java.lang.Object {

View File

@ -1,8 +1,8 @@
package sandbox.java.util; package sandbox.java.util;
/** /**
* This is a dummy class that implements just enough of [java.util.Comparator] * This is a dummy class that implements just enough of {@link java.util.Comparator}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
@FunctionalInterface @FunctionalInterface
public interface Comparator<T> extends java.util.Comparator<T> { public interface Comparator<T> extends java.util.Comparator<T> {

View File

@ -1,8 +1,8 @@
package sandbox.java.util; package sandbox.java.util;
/** /**
* This is a dummy class that implements just enough of [java.util.Locale] * This is a dummy class that implements just enough of {@link java.util.Locale}
* to allow us to compile [sandbox.java.lang.String]. * to allow us to compile {@link sandbox.java.lang.String}.
*/ */
public abstract class Locale extends sandbox.java.lang.Object { public abstract class Locale extends sandbox.java.lang.Object {
public abstract sandbox.java.lang.String toLanguageTag(); public abstract sandbox.java.lang.String toLanguageTag();

View File

@ -1,8 +1,8 @@
package sandbox.java.util.function; package sandbox.java.util.function;
/** /**
* This is a dummy class that implements just enough of [java.util.function.Function] * This is a dummy class that implements just enough of {@link java.util.function.Function}
* to allow us to compile [sandbox.Task]. * to allow us to compile {@link sandbox.Task}.
*/ */
@FunctionalInterface @FunctionalInterface
public interface Function<T, R> { public interface Function<T, R> {

View File

@ -1,8 +1,8 @@
package sandbox.java.util.function; package sandbox.java.util.function;
/** /**
* This is a dummy class that implements just enough of [java.util.function.Supplier] * This is a dummy class that implements just enough of @{link java.util.function.Supplier}
* to allow us to compile [sandbox.java.lang.ThreadLocal]. * to allow us to compile {@link sandbox.java.lang.ThreadLocal}.
*/ */
@FunctionalInterface @FunctionalInterface
public interface Supplier<T> { public interface Supplier<T> {

View File

@ -2,6 +2,7 @@ package net.corda.djvm
import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.AnalysisConfiguration
import net.corda.djvm.code.DefinitionProvider import net.corda.djvm.code.DefinitionProvider
import net.corda.djvm.code.EMIT_TRACING
import net.corda.djvm.code.Emitter import net.corda.djvm.code.Emitter
import net.corda.djvm.execution.ExecutionProfile import net.corda.djvm.execution.ExecutionProfile
import net.corda.djvm.rules.Rule import net.corda.djvm.rules.Rule
@ -51,7 +52,7 @@ class SandboxConfiguration private constructor(
executionProfile = profile, executionProfile = profile,
rules = rules, rules = rules,
emitters = (emitters ?: Discovery.find()).filter { emitters = (emitters ?: Discovery.find()).filter {
enableTracing || !it.isTracer enableTracing || it.priority > EMIT_TRACING
}, },
definitionProviders = definitionProviders, definitionProviders = definitionProviders,
analysisConfiguration = analysisConfiguration analysisConfiguration = analysisConfiguration

View File

@ -58,11 +58,21 @@ class AnalysisConfiguration(
*/ */
val stitchedInterfaces: Map<String, List<Member>> get() = STITCHED_INTERFACES 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. * Functionality used to resolve the qualified name and relevant information about a class.
*/ */
val classResolver: ClassResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX) 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) } private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) }
val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader) val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader)
@ -76,11 +86,14 @@ class AnalysisConfiguration(
fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES
fun isPinnedClass(className: String): Boolean = className in pinnedClasses 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 { companion object {
/** /**
* The package name prefix to use for classes loaded into a sandbox. * 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. * These class must belong to the application class loader.
@ -111,60 +124,162 @@ class AnalysisConfiguration(
java.lang.String.CASE_INSENSITIVE_ORDER::class.java, java.lang.String.CASE_INSENSITIVE_ORDER::class.java,
java.lang.System::class.java, java.lang.System::class.java,
java.lang.ThreadLocal::class.java, java.lang.ThreadLocal::class.java,
java.lang.Throwable::class.java,
kotlin.Any::class.java, kotlin.Any::class.java,
sun.misc.JavaLangAccess::class.java, sun.misc.JavaLangAccess::class.java,
sun.misc.SharedSecrets::class.java sun.misc.SharedSecrets::class.java
).sandboxed() + setOf( ).sandboxed() + setOf(
"sandbox/Task", "sandbox/Task",
"sandbox/TaskTypes",
"sandbox/java/lang/DJVM", "sandbox/java/lang/DJVM",
"sandbox/java/lang/DJVMException",
"sandbox/java/lang/DJVMThrowableWrapper",
"sandbox/sun/misc/SharedSecrets\$1", "sandbox/sun/misc/SharedSecrets\$1",
"sandbox/sun/misc/SharedSecrets\$JavaLangAccessImpl" "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 * These interfaces will be modified as follows when
* added to the sandbox: * added to the sandbox:
* *
* <code>interface sandbox.A extends A</code> * <code>interface sandbox.A extends A</code>
*/ */
private val STITCHED_INTERFACES: Map<String, List<Member>> = mapOf( private val STITCHED_INTERFACES: Map<String, List<Member>> = listOf(
sandboxed(CharSequence::class.java) to listOf( object : MethodBuilder(
object : MethodBuilder( access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE,
access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, className = sandboxed(CharSequence::class.java),
className = "sandbox/java/lang/CharSequence", memberName = "subSequence",
memberName = "subSequence", descriptor = "(II)Ljava/lang/CharSequence;"
descriptor = "(II)Ljava/lang/CharSequence;" ) {
) { override fun writeBody(emitter: EmitterModule) = with(emitter) {
override fun writeBody(emitter: EmitterModule) = with(emitter) { pushObject(0)
pushObject(0) pushInteger(1)
pushInteger(1) pushInteger(2)
pushInteger(2) invokeInterface(className, memberName, "(II)L$className;")
invokeInterface(className, memberName, "(II)L$className;") returnObject()
returnObject() }
} }.withBody()
}.withBody() .build(),
.build(),
MethodBuilder( MethodBuilder(
access = ACC_PUBLIC or ACC_ABSTRACT, access = ACC_PUBLIC or ACC_ABSTRACT,
className = "sandbox/java/lang/CharSequence", className = sandboxed(CharSequence::class.java),
memberName = "toString", memberName = "toString",
descriptor = "()Ljava/lang/String;" descriptor = "()Ljava/lang/String;"
).build() ).build()
), ).mapByClassName() + mapOf(
sandboxed(Comparable::class.java) to emptyList(), sandboxed(Comparable::class.java) to emptyList(),
sandboxed(Comparator::class.java) to emptyList(), sandboxed(Comparator::class.java) to emptyList(),
sandboxed(Iterable::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 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( private open class MethodBuilder(
protected val access: Int, protected val access: Int,
protected val className: String, protected val className: String,
protected val memberName: String, protected val memberName: String,
protected val descriptor: String) { protected val descriptor: String,
protected val signature: String = ""
) {
private val bodies = mutableListOf<MethodBody>() private val bodies = mutableListOf<MethodBody>()
protected open fun writeBody(emitter: EmitterModule) {} protected open fun writeBody(emitter: EmitterModule) {}
@ -179,7 +294,7 @@ class AnalysisConfiguration(
className = className, className = className,
memberName = memberName, memberName = memberName,
signature = descriptor, signature = descriptor,
genericsDetails = "", genericsDetails = signature,
body = bodies body = bodies
) )
} }

View File

@ -2,6 +2,7 @@ package net.corda.djvm.analysis
import net.corda.djvm.code.EmitterModule import net.corda.djvm.code.EmitterModule
import net.corda.djvm.code.Instruction import net.corda.djvm.code.Instruction
import net.corda.djvm.code.emptyAsNull
import net.corda.djvm.code.instructions.* import net.corda.djvm.code.instructions.*
import net.corda.djvm.messages.Message import net.corda.djvm.messages.Message
import net.corda.djvm.references.* import net.corda.djvm.references.*
@ -232,7 +233,7 @@ open class ClassAndMemberVisitor(
analysisContext.classes.add(visitedClass) analysisContext.classes.add(visitedClass)
super.visit( super.visit(
version, access, visitedClass.name, signature, version, access, visitedClass.name, signature,
visitedClass.superClass.nullIfEmpty(), visitedClass.superClass.emptyAsNull,
visitedClass.interfaces.toTypedArray() visitedClass.interfaces.toTypedArray()
) )
} }
@ -285,12 +286,19 @@ open class ClassAndMemberVisitor(
): MethodVisitor? { ): MethodVisitor? {
var visitedMember: Member? = null var visitedMember: Member? = null
val clazz = currentClass!! 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 currentMember = member
sourceLocation = sourceLocation.copy( sourceLocation = sourceLocation.copy(
memberName = name, memberName = name,
signature = desc, signature = desc,
lineNumber = 0 lineNumber = 0
) )
val processMember = captureExceptions { val processMember = captureExceptions {
visitedMember = visitMethod(clazz, member) visitedMember = visitMethod(clazz, member)
@ -320,12 +328,19 @@ open class ClassAndMemberVisitor(
): FieldVisitor? { ): FieldVisitor? {
var visitedMember: Member? = null var visitedMember: Member? = null
val clazz = currentClass!! 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 currentMember = member
sourceLocation = sourceLocation.copy( sourceLocation = sourceLocation.copy(
memberName = name, memberName = name,
signature = desc, signature = desc,
lineNumber = 0 lineNumber = 0
) )
val processMember = captureExceptions { val processMember = captureExceptions {
visitedMember = visitField(clazz, member) visitedMember = visitField(clazz, member)
@ -578,10 +593,6 @@ open class ClassAndMemberVisitor(
*/ */
const val API_VERSION: Int = Opcodes.ASM6 const val API_VERSION: Int = Opcodes.ASM6
private fun String.nullIfEmpty(): String? {
return if (this.isEmpty()) { null } else { this }
}
} }
} }

View File

@ -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
}
}

View File

@ -35,4 +35,4 @@ class PrefixTree {
return false return false
} }
} }

View File

@ -100,9 +100,6 @@ open class Whitelist private constructor(
"^java/lang/Cloneable(\\..*)?\$".toRegex(), "^java/lang/Cloneable(\\..*)?\$".toRegex(),
"^java/lang/Object(\\..*)?\$".toRegex(), "^java/lang/Object(\\..*)?\$".toRegex(),
"^java/lang/Override(\\..*)?\$".toRegex(), "^java/lang/Override(\\..*)?\$".toRegex(),
// TODO: sandbox exception handling!
"^java/lang/StackTraceElement\$".toRegex(),
"^java/lang/Throwable\$".toRegex(),
"^java/lang/Void\$".toRegex(), "^java/lang/Void\$".toRegex(),
"^java/lang/invoke/LambdaMetafactory\$".toRegex(), "^java/lang/invoke/LambdaMetafactory\$".toRegex(),
"^java/lang/invoke/MethodHandles(\\\$.*)?\$".toRegex(), "^java/lang/invoke/MethodHandles(\\\$.*)?\$".toRegex(),

View File

@ -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>() private val initializers = mutableListOf<MethodBody>()
/** /**
@ -128,8 +132,7 @@ class ClassMutator(
*/ */
override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) {
val context = EmitterContext(currentAnalysisContext(), configuration, emitter) val context = EmitterContext(currentAnalysisContext(), configuration, emitter)
// We need to apply the tracing emitters before the non-tracing ones. Processor.processEntriesOfType<Emitter>(emitters, analysisContext.messages) {
Processor.processEntriesOfType<Emitter>(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) {
it.emit(context, instruction) it.emit(context, instruction)
} }
if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) { if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) {

View File

@ -18,10 +18,10 @@ interface Emitter {
fun emit(context: EmitterContext, instruction: Instruction) 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 @JvmDefault
val isTracer: Boolean val priority: Int
get() = false get() = EMIT_DEFAULT
} }

View File

@ -168,6 +168,14 @@ class EmitterModule(
inline fun <reified T : Throwable> throwException(message: String) = throwException(T::class.java, message) 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. * Emit instruction for returning from "void" method.
*/ */

View File

@ -2,11 +2,22 @@
package net.corda.djvm.code package net.corda.djvm.code
import org.objectweb.asm.Type import org.objectweb.asm.Type
import sandbox.java.lang.DJVMException
import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.costing.ThresholdViolationError
import sandbox.net.corda.djvm.rules.RuleViolationError 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 ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java)
val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::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. * Local extension method for normalizing a class name.

View File

@ -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()

View File

@ -9,6 +9,6 @@ import org.objectweb.asm.Label
* @property handler The label of the exception handler. * @property handler The label of the exception handler.
*/ */
class TryCatchBlock( class TryCatchBlock(
val typeName: String, typeName: String,
val handler: Label handler: Label
) : NoOperationInstruction() ) : TryBlock(handler, typeName)

View File

@ -7,7 +7,6 @@ import org.objectweb.asm.Label
* *
* @property handler The handler for the finally-block. * @property handler The handler for the finally-block.
*/ */
@Suppress("MemberVisibilityCanBePrivate")
class TryFinallyBlock( class TryFinallyBlock(
val handler: Label handler: Label
) : NoOperationInstruction() ) : TryBlock(handler, "")

View File

@ -1,7 +1,6 @@
package net.corda.djvm.rewiring package net.corda.djvm.rewiring
import net.corda.djvm.SandboxConfiguration import net.corda.djvm.SandboxConfiguration
import net.corda.djvm.analysis.AnalysisConfiguration
import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.AnalysisContext
import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION
import net.corda.djvm.code.ClassMutator import net.corda.djvm.code.ClassMutator
@ -11,6 +10,8 @@ import net.corda.djvm.references.Member
import net.corda.djvm.utilities.loggerFor import net.corda.djvm.utilities.loggerFor
import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor 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. * 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 configuration: SandboxConfiguration,
private val classLoader: ClassLoader private val classLoader: ClassLoader
) { ) {
private val analysisConfig = configuration.analysisConfiguration
/** /**
* Process class and allow user to rewrite parts/all of its content through provided hooks. * Process class and allow user to rewrite parts/all of its content through provided hooks.
@ -32,13 +34,15 @@ open class ClassRewriter(
fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode { fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode {
logger.debug("Rewriting class {}...", reader.className) logger.debug("Rewriting class {}...", reader.className)
val writer = SandboxClassWriter(reader, classLoader) val writer = SandboxClassWriter(reader, classLoader)
val analysisConfiguration = configuration.analysisConfiguration val classRemapper = SandboxClassRemapper(
val classRemapper = SandboxClassRemapper(InterfaceStitcher(writer, analysisConfiguration), analysisConfiguration) ClassExceptionRemapper(SandboxStitcher(writer)),
analysisConfig
)
val visitor = ClassMutator( val visitor = ClassMutator(
classRemapper, classRemapper,
analysisConfiguration, analysisConfig,
configuration.definitionProviders, configuration.definitionProviders,
configuration.emitters configuration.emitters
) )
visitor.analyze(reader, context, options = ClassReader.EXPAND_FRAMES) visitor.analyze(reader, context, options = ClassReader.EXPAND_FRAMES)
return ByteCode(writer.toByteArray(), visitor.hasBeenModified) return ByteCode(writer.toByteArray(), visitor.hasBeenModified)
@ -50,25 +54,30 @@ open class ClassRewriter(
/** /**
* Extra visitor that is applied after [SandboxRemapper]. This "stitches" the original * 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) : ClassVisitor(API_VERSION, parent)
{ {
private val extraMethods = mutableListOf<Member>() private val extraMethods = mutableListOf<Member>()
override fun visit(version: Int, access: Int, className: String, signature: String?, superName: String?, interfaces: Array<String>?) { 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 extraMethods += methods
arrayOf(*(interfaces ?: emptyArray()), configuration.classResolver.reverse(className)) arrayOf(*(interfaces ?: emptyArray()), analysisConfig.classResolver.reverse(className))
} ?: interfaces } ?: interfaces
analysisConfig.stitchedClasses[className]?.also { methods ->
extraMethods += methods
}
super.visit(version, access, className, signature, superName, stitchedInterfaces) super.visit(version, access, className, signature, superName, stitchedInterfaces)
} }
override fun visitEnd() { override fun visitEnd() {
for (method in extraMethods) { for (method in extraMethods) {
method.apply { with(method) {
visitMethod(access, memberName, signature, genericsDetails.emptyAsNull, exceptions.toTypedArray())?.also { mv -> visitMethod(access, memberName, signature, genericsDetails.emptyAsNull, exceptions.toTypedArray())?.also { mv ->
mv.visitCode() mv.visitCode()
EmitterModule(mv).writeByteCode(body) EmitterModule(mv).writeByteCode(body)
@ -81,4 +90,26 @@ open class ClassRewriter(
super.visitEnd() 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)
}
}
} }

View File

@ -3,6 +3,9 @@ package net.corda.djvm.rewiring
import net.corda.djvm.SandboxConfiguration import net.corda.djvm.SandboxConfiguration
import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.AnalysisContext
import net.corda.djvm.analysis.ClassAndMemberVisitor 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.code.asResourcePath
import net.corda.djvm.references.ClassReference import net.corda.djvm.references.ClassReference
import net.corda.djvm.source.ClassSource import net.corda.djvm.source.ClassSource
@ -33,7 +36,7 @@ class SandboxClassLoader(
/** /**
* The analyzer used to traverse the class hierarchy. * The analyzer used to traverse the class hierarchy.
*/ */
val analyzer: ClassAndMemberVisitor private val analyzer: ClassAndMemberVisitor
get() = ruleValidator get() = ruleValidator
/** /**
@ -56,6 +59,18 @@ class SandboxClassLoader(
*/ */
private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader) 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. * Given a class name, provide its corresponding [LoadedClass] for the sandbox.
*/ */
@ -77,11 +92,43 @@ class SandboxClassLoader(
*/ */
@Throws(ClassNotFoundException::class) @Throws(ClassNotFoundException::class)
override fun loadClass(name: String, resolve: Boolean): Class<*> { override fun loadClass(name: String, resolve: Boolean): Class<*> {
val source = ClassSource.fromClassName(name) var clazz = findLoadedClass(name)
return if (name.startsWith("sandbox.") && !analysisConfiguration.isPinnedClass(source.internalClassName)) { if (clazz == null) {
loadClassAndBytes(source, context).type val source = ClassSource.fromClassName(name)
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 { } else {
super.loadClass(name, resolve) 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)
}
}
} }
} }
@ -134,7 +181,7 @@ class SandboxClassLoader(
} }
// Try to define the transformed class. // Try to define the transformed class.
val clazz = try { val clazz: Class<*> = try {
when { when {
whitelistedClasses.matches(sourceName.asResourcePath) -> supportingClassLoader.loadClass(sourceName) whitelistedClasses.matches(sourceName.asResourcePath) -> supportingClassLoader.loadClass(sourceName)
else -> defineClass(resolvedName, byteCode.bytes, 0, byteCode.bytes.size) 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 companion object {
private val logger = loggerFor<SandboxClassLoader>() private val logger = loggerFor<SandboxClassLoader>()
private val UNMODIFIED = ByteCode(ByteArray(0), false) private val UNMODIFIED = ByteCode(ByteArray(0), false)

View File

@ -3,7 +3,6 @@ package net.corda.djvm.rewiring
import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.AnalysisConfiguration
import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION
import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Label
import org.objectweb.asm.MethodVisitor import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.commons.ClassRemapper 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) 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) { override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) {
val field = Element(owner, name, descriptor) val field = Element(owner, name, descriptor)
return mapperFor(field).visitFieldInsn(opcode, owner, name, descriptor) return mapperFor(field).visitFieldInsn(opcode, owner, name, descriptor)

View File

@ -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()
}
}

View File

@ -27,30 +27,36 @@ class DisallowCatchingBlacklistedExceptions : Emitter {
companion object { companion object {
private val disallowedExceptionTypes = setOf( private val disallowedExceptionTypes = setOf(
ruleViolationError, ruleViolationError,
thresholdViolationError, thresholdViolationError,
/** /**
* These errors indicate that the JVM is failing, * These errors indicate that the JVM is failing,
* so don't allow these to be caught either. * so don't allow these to be caught either.
*/ */
"java/lang/StackOverflowError", "java/lang/StackOverflowError",
"java/lang/OutOfMemoryError", "java/lang/OutOfMemoryError",
/** /**
* These are immediate super-classes for our explicit errors. * These are immediate super-classes for our explicit errors.
*/ */
"java/lang/VirtualMachineError", "java/lang/VirtualMachineError",
"java/lang/ThreadDeath", "java/lang/ThreadDeath",
/** /**
* Any of [ThreadDeath] and [VirtualMachineError]'s throwable * Any of [ThreadDeath] and [VirtualMachineError]'s throwable
* super-classes also need explicit checking. * super-classes also need explicit checking.
*/ */
"java/lang/Throwable", "java/lang/Throwable",
"java/lang/Error" "java/lang/Error"
) )
} }
/**
* 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
} }

View File

@ -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
}

View File

@ -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;")
}
}
}
}

View File

@ -1,5 +1,6 @@
package net.corda.djvm.rules.implementation.instrumentation 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.Emitter
import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.EmitterContext
import net.corda.djvm.code.Instruction import net.corda.djvm.code.Instruction
@ -40,7 +41,7 @@ class TraceAllocations : Emitter {
} }
} }
override val isTracer: Boolean override val priority: Int
get() = true get() = EMIT_TRACING
} }

View File

@ -1,5 +1,6 @@
package net.corda.djvm.rules.implementation.instrumentation 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.Emitter
import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.EmitterContext
import net.corda.djvm.code.Instruction import net.corda.djvm.code.Instruction
@ -17,7 +18,7 @@ class TraceInvocations : Emitter {
} }
} }
override val isTracer: Boolean override val priority: Int
get() = true get() = EMIT_TRACING
} }

View File

@ -1,5 +1,6 @@
package net.corda.djvm.rules.implementation.instrumentation 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.Emitter
import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.EmitterContext
import net.corda.djvm.code.Instruction import net.corda.djvm.code.Instruction
@ -17,7 +18,7 @@ class TraceJumps : Emitter {
} }
} }
override val isTracer: Boolean override val priority: Int
get() = true get() = EMIT_TRACING
} }

View File

@ -1,5 +1,6 @@
package net.corda.djvm.rules.implementation.instrumentation 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.Emitter
import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.EmitterContext
import net.corda.djvm.code.Instruction import net.corda.djvm.code.Instruction
@ -17,7 +18,7 @@ class TraceThrows : Emitter {
} }
} }
override val isTracer: Boolean override val priority: Int
get() = true get() = EMIT_TRACING
} }

View File

@ -21,12 +21,14 @@ class ClassSource private constructor(
/** /**
* Instantiate a [ClassSource] from a fully qualified class name. * Instantiate a [ClassSource] from a fully qualified class name.
*/ */
@JvmStatic
fun fromClassName(className: String, origin: String? = null) = fun fromClassName(className: String, origin: String? = null) =
ClassSource(className, origin) ClassSource(className, origin)
/** /**
* Instantiate a [ClassSource] from a file on disk. * Instantiate a [ClassSource] from a file on disk.
*/ */
@JvmStatic
fun fromPath(path: Path) = PathClassSource(path) fun fromPath(path: Path) = PathClassSource(path)
/** /**

View File

@ -2,6 +2,8 @@ package net.corda.djvm.source
import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.AnalysisContext
import net.corda.djvm.analysis.ClassResolver 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.analysis.SourceLocation
import net.corda.djvm.code.asResourcePath import net.corda.djvm.code.asResourcePath
import net.corda.djvm.messages.Message import net.corda.djvm.messages.Message
@ -61,7 +63,15 @@ abstract class AbstractSourceClassLoader(
*/ */
override fun loadClass(name: String, resolve: Boolean): Class<*> { override fun loadClass(name: String, resolve: Boolean): Class<*> {
logger.trace("Loading class {}, resolve={}...", name, resolve) 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) return super.loadClass(originalName, resolve)
} }

View File

@ -6,7 +6,10 @@ import sandbox.java.lang.unsandbox
typealias SandboxFunction<TInput, TOutput> = sandbox.java.util.function.Function<TInput, TOutput> 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?> { class Task(private val function: SandboxFunction<in Any?, out Any?>?) : SandboxFunction<Any?, Any?> {
/** /**

View File

@ -2,19 +2,25 @@
@file:Suppress("unused") @file:Suppress("unused")
package sandbox.java.lang 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.Opcodes.ACC_ENUM
import org.objectweb.asm.Type
import sandbox.isEntryPoint
import sandbox.net.corda.djvm.rules.RuleViolationError
private const val SANDBOX_PREFIX = "sandbox." private const val SANDBOX_PREFIX = "sandbox."
fun Any.unsandbox(): Any { fun Any.unsandbox(): Any {
return when (this) { return when (this) {
is Enum<*> -> fromDJVMEnum()
is Object -> fromDJVM() is Object -> fromDJVM()
is Array<*> -> fromDJVMArray() is Array<*> -> fromDJVMArray()
else -> this else -> this
} }
} }
@Throws(ClassNotFoundException::class)
fun Any.sandbox(): Any { fun Any.sandbox(): Any {
return when (this) { return when (this) {
is kotlin.String -> String.toDJVM(this) is kotlin.String -> String.toDJVM(this)
@ -27,6 +33,7 @@ fun Any.sandbox(): Any {
is kotlin.Double -> Double.toDJVM(this) is kotlin.Double -> Double.toDJVM(this)
is kotlin.Boolean -> Boolean.toDJVM(this) is kotlin.Boolean -> Boolean.toDJVM(this)
is kotlin.Enum<*> -> toDJVMEnum() is kotlin.Enum<*> -> toDJVMEnum()
is kotlin.Throwable -> toDJVMThrowable()
is Array<*> -> toDJVMArray<Object>() is Array<*> -> toDJVMArray<Object>()
else -> this else -> this
} }
@ -38,8 +45,11 @@ private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this)
* These functions use the "current" classloader, i.e. classloader * These functions use the "current" classloader, i.e. classloader
* that owns this DJVM class. * that owns this DJVM class.
*/ */
private fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) @Throws(ClassNotFoundException::class)
private fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) 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 { private fun kotlin.String.toSandboxPackage(): kotlin.String {
return if (startsWith(SANDBOX_PREFIX)) { 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<*> return javaClass.fromDJVMType().enumConstants[ordinal()] as kotlin.Enum<*>
} }
@Throws(ClassNotFoundException::class)
private fun kotlin.Enum<*>.toDJVMEnum(): Enum<*> { private fun kotlin.Enum<*>.toDJVMEnum(): Enum<*> {
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
return (getEnumConstants(javaClass.toDJVMType() as Class<Enum<*>>) as Array<Enum<*>>)[ordinal] 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<*>>? { 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. // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map.
@Suppress("ReplaceGetOrSet")
return allEnumDirectories.get(clazz) ?: createEnumDirectory(clazz) return allEnumDirectories.get(clazz) ?: createEnumDirectory(clazz)
} }
@Suppress("unchecked_cast") @Suppress("unchecked_cast", "ReplaceGetOrSet")
internal fun getEnumConstantsShared(clazz: Class<out Enum<*>>): Array<out Enum<*>>? { internal fun getEnumConstantsShared(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
return if (isEnum(clazz)) { return if (isEnum(clazz)) {
// DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. // 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<*>>? { private fun createEnum(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
return clazz.getMethod("values").let { method -> return clazz.getMethod("values").let { method ->
method.isAccessible = true method.isAccessible = true
@ -109,6 +122,7 @@ private fun createEnum(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
}?.apply { allEnums.put(clazz, this) } }?.apply { allEnums.put(clazz, this) }
} }
@Suppress("ReplacePutWithAssignment")
private fun createEnumDirectory(clazz: Class<out Enum<*>>): sandbox.java.util.Map<String, out Enum<*>> { 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 universe = getEnumConstantsShared(clazz) ?: throw IllegalArgumentException("${clazz.name} is not an enum type")
val directory = sandbox.java.util.LinkedHashMap<String, Enum<*>>(2 * universe.size) 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( private val bannedClasses = setOf(
"^java\\.lang\\.DJVM(.*)?\$".toRegex(), "^java\\.lang\\.DJVM(.*)?\$".toRegex(),
"^net\\.corda\\.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

View 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
}

View File

@ -6,4 +6,4 @@ package sandbox.net.corda.djvm.costing
* *
* @property message The description of the condition causing the problem. * @property message The description of the condition causing the problem.
*/ */
class ThresholdViolationError(override val message: String) : ThreadDeath() class ThresholdViolationError(override val message: String?) : ThreadDeath()

View File

@ -7,4 +7,4 @@ package sandbox.net.corda.djvm.rules
* *
* @property message The description of the condition causing the problem. * @property message The description of the condition causing the problem.
*/ */
class RuleViolationError(override val message: String) : ThreadDeath() class RuleViolationError(override val message: String?) : ThreadDeath()

View 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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View 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()
}

View File

@ -113,14 +113,4 @@ class DJVMTest {
assertArrayEquals(ByteArray(1) { 127.toByte() }, result[9] as ByteArray) assertArrayEquals(ByteArray(1) { 127.toByte() }, result[9] as ByteArray)
assertArrayEquals(CharArray(1) { '?' }, result[10] as CharArray) 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)
} }

View File

@ -37,22 +37,29 @@ abstract class TestBase {
val ALL_EMITTERS = Discovery.find<Emitter>() val ALL_EMITTERS = Discovery.find<Emitter>()
// We need at least these emitters to handle the Java API classes. // We need at least these emitters to handle the Java API classes.
@JvmField
val BASIC_EMITTERS: List<Emitter> = listOf( val BASIC_EMITTERS: List<Emitter> = listOf(
ArgumentUnwrapper(), ArgumentUnwrapper(),
HandleExceptionUnwrapper(),
ReturnTypeWrapper(), ReturnTypeWrapper(),
RewriteClassMethods(), RewriteClassMethods(),
StringConstantWrapper() StringConstantWrapper(),
ThrowExceptionWrapper()
) )
val ALL_DEFINITION_PROVIDERS = Discovery.find<DefinitionProvider>() val ALL_DEFINITION_PROVIDERS = Discovery.find<DefinitionProvider>()
// We need at least these providers to handle the Java API classes. // We need at least these providers to handle the Java API classes.
@JvmField
val BASIC_DEFINITION_PROVIDERS: List<DefinitionProvider> = listOf(StaticConstantRemover()) val BASIC_DEFINITION_PROVIDERS: List<DefinitionProvider> = listOf(StaticConstantRemover())
@JvmField
val BLANK = emptySet<Any>() val BLANK = emptySet<Any>()
@JvmField
val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass) val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass)
@JvmField
val DETERMINISTIC_RT: Path = Paths.get( val DETERMINISTIC_RT: Path = Paths.get(
System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set")) 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) val reader = ClassReader(T::class.java.name)
AnalysisConfiguration( AnalysisConfiguration(
minimumSeverityLevel = minimumSeverityLevel, minimumSeverityLevel = minimumSeverityLevel,
classPath = listOf(DETERMINISTIC_RT) bootstrapJar = DETERMINISTIC_RT
).use { analysisConfiguration -> ).use { analysisConfiguration ->
val validator = RuleValidator(ALL_RULES, analysisConfiguration) val validator = RuleValidator(ALL_RULES, analysisConfiguration)
val context = AnalysisContext.fromConfiguration(analysisConfiguration) val context = AnalysisContext.fromConfiguration(analysisConfiguration)

View File

@ -1,22 +1,24 @@
@file:JvmName("UtilityFunctions")
package net.corda.djvm package net.corda.djvm
import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.costing.ThresholdViolationError
import sandbox.net.corda.djvm.rules.RuleViolationError import sandbox.net.corda.djvm.rules.RuleViolationError
/**
* Allows us to create a [Utilities] object that we can pin inside the sandbox.
*/
object Utilities { object Utilities {
fun throwRuleViolationError(): Nothing = throw RuleViolationError("Can't catch this!") fun throwRuleViolationError(): Nothing = throw RuleViolationError("Can't catch this!")
fun throwThresholdViolationError(): Nothing = throw ThresholdViolationError("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)

View File

@ -6,14 +6,8 @@ import foo.bar.sandbox.toNumber
import net.corda.djvm.TestBase import net.corda.djvm.TestBase
import net.corda.djvm.analysis.Whitelist import net.corda.djvm.analysis.Whitelist
import net.corda.djvm.Utilities 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.throwRuleViolationError
import net.corda.djvm.Utilities.throwStackOverflowError
import net.corda.djvm.Utilities.throwThreadDeath
import net.corda.djvm.Utilities.throwThresholdViolationError import net.corda.djvm.Utilities.throwThresholdViolationError
import net.corda.djvm.Utilities.throwThrowable
import net.corda.djvm.assertions.AssertionExtensions.withProblem import net.corda.djvm.assertions.AssertionExtensions.withProblem
import net.corda.djvm.rewiring.SandboxClassLoadingException import net.corda.djvm.rewiring.SandboxClassLoadingException
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -55,7 +49,7 @@ class SandboxExecutorTest : TestBase() {
class Contract : Function<Transaction, Unit> { class Contract : Function<Transaction, Unit> {
override fun apply(input: Transaction) { override fun apply(input: Transaction) {
throwContractConstraintViolation() throw IllegalArgumentException("Contract constraint violated")
} }
} }
@ -74,11 +68,7 @@ class SandboxExecutorTest : TestBase() {
val obj = Object() val obj = Object()
val hash1 = obj.hashCode() val hash1 = obj.hashCode()
val hash2 = obj.hashCode() val hash2 = obj.hashCode()
//require(hash1 == hash2) require(hash1 == hash2)
// TODO: Replace require() once we have working exception support.
if (hash1 != hash2) {
throwError()
}
return Object().hashCode() return Object().hashCode()
} }
} }
@ -180,7 +170,7 @@ class SandboxExecutorTest : TestBase() {
class TestCatchThreadDeath : Function<Int, Int> { class TestCatchThreadDeath : Function<Int, Int> {
override fun apply(input: Int): Int { override fun apply(input: Int): Int {
return try { return try {
throwThreadDeath() throw ThreadDeath()
} catch (exception: ThreadDeath) { } catch (exception: ThreadDeath) {
1 1
} }
@ -261,8 +251,8 @@ class SandboxExecutorTest : TestBase() {
override fun apply(input: Int): Int { override fun apply(input: Int): Int {
return try { return try {
when (input) { when (input) {
1 -> throwThrowable() 1 -> throw Throwable()
2 -> throwError() 2 -> throw Error()
else -> 0 else -> 0
} }
} catch (exception: Error) { } catch (exception: Error) {
@ -277,20 +267,20 @@ class SandboxExecutorTest : TestBase() {
override fun apply(input: Int): Int { override fun apply(input: Int): Int {
return try { return try {
when (input) { when (input) {
1 -> throwThrowable() 1 -> throw Throwable()
2 -> throwError() 2 -> throw Error()
3 -> try { 3 -> try {
throwThreadDeath() throw ThreadDeath()
} catch (ex: ThreadDeath) { } catch (ex: ThreadDeath) {
3 3
} }
4 -> try { 4 -> try {
throwStackOverflowError() throw StackOverflowError("FAKE OVERFLOW!")
} catch (ex: StackOverflowError) { } catch (ex: StackOverflowError) {
4 4
} }
5 -> try { 5 -> try {
throwOutOfMemoryError() throw OutOfMemoryError("FAKE OOM!")
} catch (ex: OutOfMemoryError) { } catch (ex: OutOfMemoryError) {
5 5
} }

View File

@ -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)