From e10119031cdc0b4b62068dcd2e70b98884d05e2c Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 19 Oct 2018 17:23:14 +0100 Subject: [PATCH] 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. --- djvm/build.gradle | 2 +- .../java/sandbox/java/lang/Appendable.java | 8 +- .../java/sandbox/java/lang/CharSequence.java | 4 +- .../java/sandbox/java/lang/Comparable.java | 4 +- .../java/lang/DJVMThrowableWrapper.java | 37 ++++ .../src/main/java/sandbox/java/lang/Enum.java | 10 +- .../main/java/sandbox/java/lang/Iterable.java | 4 +- .../main/java/sandbox/java/lang/Object.java | 3 +- .../sandbox/java/lang/StackTraceElement.java | 46 +++++ .../main/java/sandbox/java/lang/String.java | 27 +++ .../java/sandbox/java/lang/StringBuffer.java | 4 +- .../java/sandbox/java/lang/StringBuilder.java | 4 +- .../java/sandbox/java/lang/Throwable.java | 137 ++++++++++++++ .../sandbox/java/nio/charset/Charset.java | 4 +- .../java/sandbox/java/util/Comparator.java | 4 +- .../main/java/sandbox/java/util/Locale.java | 4 +- .../sandbox/java/util/function/Function.java | 4 +- .../sandbox/java/util/function/Supplier.java | 4 +- .../net/corda/djvm/SandboxConfiguration.kt | 3 +- .../djvm/analysis/AnalysisConfiguration.kt | 171 +++++++++++++++--- .../djvm/analysis/ClassAndMemberVisitor.kt | 37 ++-- .../corda/djvm/analysis/ExceptionResolver.kt | 41 +++++ .../net/corda/djvm/analysis/PrefixTree.kt | 2 +- .../net/corda/djvm/analysis/Whitelist.kt | 3 - .../net/corda/djvm/code/ClassMutator.kt | 9 +- .../kotlin/net/corda/djvm/code/Emitter.kt | 6 +- .../net/corda/djvm/code/EmitterModule.kt | 8 + .../main/kotlin/net/corda/djvm/code/Types.kt | 11 ++ .../corda/djvm/code/instructions/TryBlock.kt | 8 + .../djvm/code/instructions/TryCatchBlock.kt | 6 +- .../djvm/code/instructions/TryFinallyBlock.kt | 5 +- .../net/corda/djvm/rewiring/ClassRewriter.kt | 55 ++++-- .../corda/djvm/rewiring/SandboxClassLoader.kt | 68 ++++++- .../djvm/rewiring/SandboxClassRemapper.kt | 6 - .../djvm/rewiring/ThrowableWrapperFactory.kt | 152 ++++++++++++++++ .../DisallowCatchingBlacklistedExceptions.kt | 44 +++-- .../HandleExceptionUnwrapper.kt | 46 +++++ .../implementation/ThrowExceptionWrapper.kt | 20 ++ .../instrumentation/TraceAllocations.kt | 5 +- .../instrumentation/TraceInvocations.kt | 5 +- .../instrumentation/TraceJumps.kt | 5 +- .../instrumentation/TraceThrows.kt | 5 +- .../net/corda/djvm/source/ClassSource.kt | 2 + .../corda/djvm/source/SourceClassLoader.kt | 12 +- djvm/src/main/kotlin/sandbox/Task.kt | 5 +- .../src/main/kotlin/sandbox/java/lang/DJVM.kt | 153 +++++++++++++++- .../kotlin/sandbox/java/lang/DJVMException.kt | 12 ++ .../djvm/costing/ThresholdViolationError.kt | 2 +- .../corda/djvm/rules/RuleViolationError.kt | 2 +- .../test/java/net/corda/djvm/WithJava.java | 24 +++ .../djvm/execution/SandboxEnumJavaTest.java | 106 +++++++++++ .../execution/SandboxThrowableJavaTest.java | 79 ++++++++ .../net/corda/djvm/DJVMExceptionTest.kt | 100 ++++++++++ .../test/kotlin/net/corda/djvm/DJVMTest.kt | 10 - .../test/kotlin/net/corda/djvm/TestBase.kt | 11 +- .../test/kotlin/net/corda/djvm/Utilities.kt | 26 +-- .../djvm/execution/SandboxExecutorTest.kt | 30 +-- .../djvm/execution/SandboxThrowableTest.kt | 95 ++++++++++ 58 files changed, 1508 insertions(+), 192 deletions(-) create mode 100644 djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java create mode 100644 djvm/src/main/java/sandbox/java/lang/StackTraceElement.java create mode 100644 djvm/src/main/java/sandbox/java/lang/Throwable.java create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt create mode 100644 djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt create mode 100644 djvm/src/test/java/net/corda/djvm/WithJava.java create mode 100644 djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java create mode 100644 djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java create mode 100644 djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt diff --git a/djvm/build.gradle b/djvm/build.gradle index d40a4d5523..1b33bdd3ae 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -33,7 +33,6 @@ dependencies { // ASM: byte code manipulation library compile "org.ow2.asm:asm:$asm_version" - compile "org.ow2.asm:asm-tree:$asm_version" compile "org.ow2.asm:asm-commons:$asm_version" // ClassGraph: classpath scanning @@ -62,6 +61,7 @@ shadowJar { exclude 'sandbox/java/lang/Comparable.class' exclude 'sandbox/java/lang/Enum.class' exclude 'sandbox/java/lang/Iterable.class' + exclude 'sandbox/java/lang/StackTraceElement.class' exclude 'sandbox/java/lang/StringBuffer.class' exclude 'sandbox/java/lang/StringBuilder.class' exclude 'sandbox/java/nio/**' diff --git a/djvm/src/main/java/sandbox/java/lang/Appendable.java b/djvm/src/main/java/sandbox/java/lang/Appendable.java index 168607c511..c95eaf6e53 100644 --- a/djvm/src/main/java/sandbox/java/lang/Appendable.java +++ b/djvm/src/main/java/sandbox/java/lang/Appendable.java @@ -3,10 +3,10 @@ package sandbox.java.lang; import java.io.IOException; /** - * This is a dummy class that implements just enough of [java.lang.Appendable] - * to keep [sandbox.java.lang.StringBuilder], [sandbox.java.lang.StringBuffer] - * and [sandbox.java.lang.String] honest. - * Note that it does not extend [java.lang.Appendable]. + * This is a dummy class that implements just enough of {@link java.lang.Appendable} + * to keep {@link sandbox.java.lang.StringBuilder}, {@link sandbox.java.lang.StringBuffer} + * and {@link sandbox.java.lang.String} honest. + * Note that it does not extend {@link java.lang.Appendable}. */ public interface Appendable { diff --git a/djvm/src/main/java/sandbox/java/lang/CharSequence.java b/djvm/src/main/java/sandbox/java/lang/CharSequence.java index 1847103093..10b024d027 100644 --- a/djvm/src/main/java/sandbox/java/lang/CharSequence.java +++ b/djvm/src/main/java/sandbox/java/lang/CharSequence.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import org.jetbrains.annotations.NotNull; /** - * This is a dummy class that implements just enough of [java.lang.CharSequence] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.CharSequence} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface CharSequence extends java.lang.CharSequence { diff --git a/djvm/src/main/java/sandbox/java/lang/Comparable.java b/djvm/src/main/java/sandbox/java/lang/Comparable.java index 686539c1b4..59b3278a1b 100644 --- a/djvm/src/main/java/sandbox/java/lang/Comparable.java +++ b/djvm/src/main/java/sandbox/java/lang/Comparable.java @@ -1,8 +1,8 @@ package sandbox.java.lang; /** - * This is a dummy class that implements just enough of [java.lang.Comparable] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.Comparable} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface Comparable extends java.lang.Comparable { } diff --git a/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java b/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java new file mode 100644 index 0000000000..60de2e117d --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java @@ -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; + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Enum.java b/djvm/src/main/java/sandbox/java/lang/Enum.java index ffcdd8c916..d3a4bf352e 100644 --- a/djvm/src/main/java/sandbox/java/lang/Enum.java +++ b/djvm/src/main/java/sandbox/java/lang/Enum.java @@ -1,11 +1,13 @@ package sandbox.java.lang; +import org.jetbrains.annotations.NotNull; + import java.io.Serializable; /** * This is a dummy class. We will load the actual Enum class at run-time. */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "WeakerAccess"}) public abstract class Enum> extends Object implements Comparable, Serializable { private final String name; @@ -24,4 +26,10 @@ public abstract class Enum> extends Object implements Comparab return ordinal; } + @Override + @NotNull + final java.lang.Enum fromDJVM() { + throw new UnsupportedOperationException("Dummy implementation"); + } + } diff --git a/djvm/src/main/java/sandbox/java/lang/Iterable.java b/djvm/src/main/java/sandbox/java/lang/Iterable.java index 6032fd97db..01f8108ac0 100644 --- a/djvm/src/main/java/sandbox/java/lang/Iterable.java +++ b/djvm/src/main/java/sandbox/java/lang/Iterable.java @@ -5,8 +5,8 @@ import org.jetbrains.annotations.NotNull; import java.util.Iterator; /** - * This is a dummy class that implements just enough of [java.lang.Iterable] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.Iterable} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface Iterable extends java.lang.Iterable { @Override diff --git a/djvm/src/main/java/sandbox/java/lang/Object.java b/djvm/src/main/java/sandbox/java/lang/Object.java index 4208a52a53..62ac16d4dd 100644 --- a/djvm/src/main/java/sandbox/java/lang/Object.java +++ b/djvm/src/main/java/sandbox/java/lang/Object.java @@ -54,8 +54,7 @@ public class Object { private static Class fromDJVM(Class type) { try { - java.lang.String name = type.getName(); - return Class.forName(name.startsWith("sandbox.") ? name.substring(8) : name); + return DJVM.fromDJVMType(type); } catch (ClassNotFoundException e) { throw new RuleViolationError(e.getMessage()); } diff --git a/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java b/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java new file mode 100644 index 0000000000..7b8173134a --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java @@ -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 + ')' : "") + ); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/String.java b/djvm/src/main/java/sandbox/java/lang/String.java index 4cce494d30..476669bfe9 100644 --- a/djvm/src/main/java/sandbox/java/lang/String.java +++ b/djvm/src/main/java/sandbox/java/lang/String.java @@ -7,6 +7,8 @@ import sandbox.java.util.Locale; import java.io.Serializable; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.util.Map; @SuppressWarnings("unused") public final class String extends Object implements Comparable, CharSequence, Serializable { @@ -22,6 +24,18 @@ public final class String extends Object implements Comparable, CharSequ private static final String TRUE = new String("true"); private static final String FALSE = new String("false"); + private static final Map INTERNAL = new java.util.HashMap<>(); + private static final Constructor SHARED; + + static { + try { + SHARED = java.lang.String.class.getDeclaredConstructor(char[].class, java.lang.Boolean.TYPE); + SHARED.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + } + private final java.lang.String value; public String() { @@ -88,6 +102,17 @@ public final class String extends Object implements Comparable, CharSequ this.value = builder.toString(); } + String(char[] value, boolean share) { + java.lang.String newValue; + try { + // This is (presumably) an optimisation for memory usage. + newValue = (java.lang.String) SHARED.newInstance(value, share); + } catch (Exception e) { + newValue = new java.lang.String(value); + } + this.value = newValue; + } + @Override public char charAt(int index) { return value.charAt(index); @@ -310,6 +335,8 @@ public final class String extends Object implements Comparable, CharSequ return toDJVM(value.trim()); } + public String intern() { return INTERNAL.computeIfAbsent(value, s -> this); } + public char[] toCharArray() { return value.toCharArray(); } diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuffer.java b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java index e9cbcad328..4d8fea7e1d 100644 --- a/djvm/src/main/java/sandbox/java/lang/StringBuffer.java +++ b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import java.io.Serializable; /** - * This is a dummy class that implements just enough of [java.lang.StringBuffer] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.StringBuffer} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class StringBuffer extends Object implements CharSequence, Appendable, Serializable { diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuilder.java b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java index ed80b2e508..a90fef7dde 100644 --- a/djvm/src/main/java/sandbox/java/lang/StringBuilder.java +++ b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import java.io.Serializable; /** - * This is a dummy class that implements just enough of [java.lang.StringBuilder] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.StringBuilder} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class StringBuilder extends Object implements Appendable, CharSequence, Serializable { diff --git a/djvm/src/main/java/sandbox/java/lang/Throwable.java b/djvm/src/main/java/sandbox/java/lang/Throwable.java new file mode 100644 index 0000000000..df19e2fc6e --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Throwable.java @@ -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(""); + } + + public void printStackTrace() {} + + @Override + @NotNull + java.lang.Throwable fromDJVM() { + return DJVM.fromDJVM(this); + } +} diff --git a/djvm/src/main/java/sandbox/java/nio/charset/Charset.java b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java index 371a21404a..453006bb7f 100644 --- a/djvm/src/main/java/sandbox/java/nio/charset/Charset.java +++ b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java @@ -1,8 +1,8 @@ package sandbox.java.nio.charset; /** - * This is a dummy class that implements just enough of [java.nio.charset.Charset] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.nio.charset.Charset} + * to allow us to compile {@link sandbox.java.lang.String}. */ @SuppressWarnings("unused") public abstract class Charset extends sandbox.java.lang.Object { diff --git a/djvm/src/main/java/sandbox/java/util/Comparator.java b/djvm/src/main/java/sandbox/java/util/Comparator.java index 20679dee59..f6363d9c34 100644 --- a/djvm/src/main/java/sandbox/java/util/Comparator.java +++ b/djvm/src/main/java/sandbox/java/util/Comparator.java @@ -1,8 +1,8 @@ package sandbox.java.util; /** - * This is a dummy class that implements just enough of [java.util.Comparator] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.util.Comparator} + * to allow us to compile {@link sandbox.java.lang.String}. */ @FunctionalInterface public interface Comparator extends java.util.Comparator { diff --git a/djvm/src/main/java/sandbox/java/util/Locale.java b/djvm/src/main/java/sandbox/java/util/Locale.java index 3ceaea9382..ed06b79058 100644 --- a/djvm/src/main/java/sandbox/java/util/Locale.java +++ b/djvm/src/main/java/sandbox/java/util/Locale.java @@ -1,8 +1,8 @@ package sandbox.java.util; /** - * This is a dummy class that implements just enough of [java.util.Locale] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.util.Locale} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class Locale extends sandbox.java.lang.Object { public abstract sandbox.java.lang.String toLanguageTag(); diff --git a/djvm/src/main/java/sandbox/java/util/function/Function.java b/djvm/src/main/java/sandbox/java/util/function/Function.java index 5cd806a01e..ce26393f67 100644 --- a/djvm/src/main/java/sandbox/java/util/function/Function.java +++ b/djvm/src/main/java/sandbox/java/util/function/Function.java @@ -1,8 +1,8 @@ package sandbox.java.util.function; /** - * This is a dummy class that implements just enough of [java.util.function.Function] - * to allow us to compile [sandbox.Task]. + * This is a dummy class that implements just enough of {@link java.util.function.Function} + * to allow us to compile {@link sandbox.Task}. */ @FunctionalInterface public interface Function { diff --git a/djvm/src/main/java/sandbox/java/util/function/Supplier.java b/djvm/src/main/java/sandbox/java/util/function/Supplier.java index 31f236bae6..0ff9f56dfb 100644 --- a/djvm/src/main/java/sandbox/java/util/function/Supplier.java +++ b/djvm/src/main/java/sandbox/java/util/function/Supplier.java @@ -1,8 +1,8 @@ package sandbox.java.util.function; /** - * This is a dummy class that implements just enough of [java.util.function.Supplier] - * to allow us to compile [sandbox.java.lang.ThreadLocal]. + * This is a dummy class that implements just enough of @{link java.util.function.Supplier} + * to allow us to compile {@link sandbox.java.lang.ThreadLocal}. */ @FunctionalInterface public interface Supplier { diff --git a/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt index da7cd0d553..ffd233df25 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt @@ -2,6 +2,7 @@ package net.corda.djvm import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.code.DefinitionProvider +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.execution.ExecutionProfile import net.corda.djvm.rules.Rule @@ -51,7 +52,7 @@ class SandboxConfiguration private constructor( executionProfile = profile, rules = rules, emitters = (emitters ?: Discovery.find()).filter { - enableTracing || !it.isTracer + enableTracing || it.priority > EMIT_TRACING }, definitionProviders = definitionProviders, analysisConfiguration = analysisConfiguration diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt index f8d87fd1ea..ff71421a54 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt @@ -58,11 +58,21 @@ class AnalysisConfiguration( */ val stitchedInterfaces: Map> get() = STITCHED_INTERFACES + /** + * These classes have extra methods added as they are mapped into the sandbox. + */ + val stitchedClasses: Map> get() = STITCHED_CLASSES + /** * Functionality used to resolve the qualified name and relevant information about a class. */ val classResolver: ClassResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX) + /** + * Resolves the internal names of synthetic exception classes. + */ + val exceptionResolver: ExceptionResolver = ExceptionResolver(JVM_EXCEPTIONS, pinnedClasses, SANDBOX_PREFIX) + private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) } val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader) @@ -76,11 +86,14 @@ class AnalysisConfiguration( fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES fun isPinnedClass(className: String): Boolean = className in pinnedClasses + fun isJvmException(className: String): Boolean = className in JVM_EXCEPTIONS + fun isSandboxClass(className: String): Boolean = className.startsWith(SANDBOX_PREFIX) && !isPinnedClass(className) + companion object { /** * The package name prefix to use for classes loaded into a sandbox. */ - private const val SANDBOX_PREFIX: String = "sandbox/" + const val SANDBOX_PREFIX: String = "sandbox/" /** * These class must belong to the application class loader. @@ -111,60 +124,162 @@ class AnalysisConfiguration( java.lang.String.CASE_INSENSITIVE_ORDER::class.java, java.lang.System::class.java, java.lang.ThreadLocal::class.java, + java.lang.Throwable::class.java, kotlin.Any::class.java, sun.misc.JavaLangAccess::class.java, sun.misc.SharedSecrets::class.java ).sandboxed() + setOf( "sandbox/Task", + "sandbox/TaskTypes", "sandbox/java/lang/DJVM", + "sandbox/java/lang/DJVMException", + "sandbox/java/lang/DJVMThrowableWrapper", "sandbox/sun/misc/SharedSecrets\$1", "sandbox/sun/misc/SharedSecrets\$JavaLangAccessImpl" ) + /** + * These are thrown by the JVM itself, and so + * we need to handle them without wrapping them. + * + * Note that this set is closed, i.e. every one + * of these exceptions' [Throwable] super classes + * is also within this set. + * + * The full list of exceptions is determined by: + * hotspot/src/share/vm/classfile/vmSymbols.hpp + */ + val JVM_EXCEPTIONS: Set = setOf( + java.io.IOException::class.java, + java.lang.AbstractMethodError::class.java, + java.lang.ArithmeticException::class.java, + java.lang.ArrayIndexOutOfBoundsException::class.java, + java.lang.ArrayStoreException::class.java, + java.lang.ClassCastException::class.java, + java.lang.ClassCircularityError::class.java, + java.lang.ClassFormatError::class.java, + java.lang.ClassNotFoundException::class.java, + java.lang.CloneNotSupportedException::class.java, + java.lang.Error::class.java, + java.lang.Exception::class.java, + java.lang.ExceptionInInitializerError::class.java, + java.lang.IllegalAccessError::class.java, + java.lang.IllegalAccessException::class.java, + java.lang.IllegalArgumentException::class.java, + java.lang.IllegalStateException::class.java, + java.lang.IncompatibleClassChangeError::class.java, + java.lang.IndexOutOfBoundsException::class.java, + java.lang.InstantiationError::class.java, + java.lang.InstantiationException::class.java, + java.lang.InternalError::class.java, + java.lang.LinkageError::class.java, + java.lang.NegativeArraySizeException::class.java, + java.lang.NoClassDefFoundError::class.java, + java.lang.NoSuchFieldError::class.java, + java.lang.NoSuchFieldException::class.java, + java.lang.NoSuchMethodError::class.java, + java.lang.NoSuchMethodException::class.java, + java.lang.NullPointerException::class.java, + java.lang.OutOfMemoryError::class.java, + java.lang.ReflectiveOperationException::class.java, + java.lang.RuntimeException::class.java, + java.lang.StackOverflowError::class.java, + java.lang.StringIndexOutOfBoundsException::class.java, + java.lang.ThreadDeath::class.java, + java.lang.Throwable::class.java, + java.lang.UnknownError::class.java, + java.lang.UnsatisfiedLinkError::class.java, + java.lang.UnsupportedClassVersionError::class.java, + java.lang.UnsupportedOperationException::class.java, + java.lang.VerifyError::class.java, + java.lang.VirtualMachineError::class.java + ).sandboxed() + setOf( + // Mentioned here to prevent the DJVM from generating a synthetic wrapper. + "sandbox/java/lang/DJVMThrowableWrapper" + ) + /** * These interfaces will be modified as follows when * added to the sandbox: * * interface sandbox.A extends A */ - private val STITCHED_INTERFACES: Map> = mapOf( - sandboxed(CharSequence::class.java) to listOf( - object : MethodBuilder( - access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, - className = "sandbox/java/lang/CharSequence", - memberName = "subSequence", - descriptor = "(II)Ljava/lang/CharSequence;" - ) { - override fun writeBody(emitter: EmitterModule) = with(emitter) { - pushObject(0) - pushInteger(1) - pushInteger(2) - invokeInterface(className, memberName, "(II)L$className;") - returnObject() - } - }.withBody() - .build(), - MethodBuilder( - access = ACC_PUBLIC or ACC_ABSTRACT, - className = "sandbox/java/lang/CharSequence", - memberName = "toString", - descriptor = "()Ljava/lang/String;" - ).build() - ), + private val STITCHED_INTERFACES: Map> = listOf( + object : MethodBuilder( + access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, + className = sandboxed(CharSequence::class.java), + memberName = "subSequence", + descriptor = "(II)Ljava/lang/CharSequence;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + pushInteger(1) + pushInteger(2) + invokeInterface(className, memberName, "(II)L$className;") + returnObject() + } + }.withBody() + .build(), + + MethodBuilder( + access = ACC_PUBLIC or ACC_ABSTRACT, + className = sandboxed(CharSequence::class.java), + memberName = "toString", + descriptor = "()Ljava/lang/String;" + ).build() + ).mapByClassName() + mapOf( sandboxed(Comparable::class.java) to emptyList(), sandboxed(Comparator::class.java) to emptyList(), sandboxed(Iterable::class.java) to emptyList() ) - private fun sandboxed(clazz: Class<*>) = SANDBOX_PREFIX + Type.getInternalName(clazz) + /** + * These classes have extra methods added when mapped into the sandbox. + */ + private val STITCHED_CLASSES: Map> = 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>.sandboxed(): Set = map(Companion::sandboxed).toSet() + private fun Iterable.mapByClassName(): Map> + = groupBy(Member::className).mapValues(Map.Entry>::value) } private open class MethodBuilder( protected val access: Int, protected val className: String, protected val memberName: String, - protected val descriptor: String) { + protected val descriptor: String, + protected val signature: String = "" + ) { private val bodies = mutableListOf() protected open fun writeBody(emitter: EmitterModule) {} @@ -179,7 +294,7 @@ class AnalysisConfiguration( className = className, memberName = memberName, signature = descriptor, - genericsDetails = "", + genericsDetails = signature, body = bodies ) } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt index 8bfb997ae7..3ab04895e4 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt @@ -2,6 +2,7 @@ package net.corda.djvm.analysis import net.corda.djvm.code.EmitterModule import net.corda.djvm.code.Instruction +import net.corda.djvm.code.emptyAsNull import net.corda.djvm.code.instructions.* import net.corda.djvm.messages.Message import net.corda.djvm.references.* @@ -232,7 +233,7 @@ open class ClassAndMemberVisitor( analysisContext.classes.add(visitedClass) super.visit( version, access, visitedClass.name, signature, - visitedClass.superClass.nullIfEmpty(), + visitedClass.superClass.emptyAsNull, visitedClass.interfaces.toTypedArray() ) } @@ -285,12 +286,19 @@ open class ClassAndMemberVisitor( ): MethodVisitor? { var visitedMember: Member? = null val clazz = currentClass!! - val member = Member(access, clazz.name, name, desc, signature ?: "") + val member = Member( + access = access, + className = clazz.name, + memberName = name, + signature = desc, + genericsDetails = signature ?: "", + exceptions = exceptions?.toMutableSet() ?: mutableSetOf() + ) currentMember = member sourceLocation = sourceLocation.copy( - memberName = name, - signature = desc, - lineNumber = 0 + memberName = name, + signature = desc, + lineNumber = 0 ) val processMember = captureExceptions { visitedMember = visitMethod(clazz, member) @@ -320,12 +328,19 @@ open class ClassAndMemberVisitor( ): FieldVisitor? { var visitedMember: Member? = null val clazz = currentClass!! - val member = Member(access, clazz.name, name, desc, "", value = value) + val member = Member( + access = access, + className = clazz.name, + memberName = name, + signature = desc, + genericsDetails = "", + value = value + ) currentMember = member sourceLocation = sourceLocation.copy( - memberName = name, - signature = desc, - lineNumber = 0 + memberName = name, + signature = desc, + lineNumber = 0 ) val processMember = captureExceptions { visitedMember = visitField(clazz, member) @@ -578,10 +593,6 @@ open class ClassAndMemberVisitor( */ const val API_VERSION: Int = Opcodes.ASM6 - private fun String.nullIfEmpty(): String? { - return if (this.isEmpty()) { null } else { this } - } - } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt new file mode 100644 index 0000000000..0bfeb103e2 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt @@ -0,0 +1,41 @@ +package net.corda.djvm.analysis + +import org.objectweb.asm.Type + +class ExceptionResolver( + private val jvmExceptionClasses: Set, + private val pinnedClasses: Set, + 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 + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt index a063965b76..26679cc133 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt @@ -35,4 +35,4 @@ class PrefixTree { return false } -} \ No newline at end of file +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt index c19cc8111e..ed3fb32e88 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt @@ -100,9 +100,6 @@ open class Whitelist private constructor( "^java/lang/Cloneable(\\..*)?\$".toRegex(), "^java/lang/Object(\\..*)?\$".toRegex(), "^java/lang/Override(\\..*)?\$".toRegex(), - // TODO: sandbox exception handling! - "^java/lang/StackTraceElement\$".toRegex(), - "^java/lang/Throwable\$".toRegex(), "^java/lang/Void\$".toRegex(), "^java/lang/invoke/LambdaMetafactory\$".toRegex(), "^java/lang/invoke/MethodHandles(\\\$.*)?\$".toRegex(), diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt index 3c800d9859..4d8f2b6307 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt @@ -41,7 +41,11 @@ class ClassMutator( } } - private val emitters: List = 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 = (emitters + PrependClassInitializer()).sortedBy(Emitter::priority) private val initializers = mutableListOf() /** @@ -128,8 +132,7 @@ class ClassMutator( */ override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { val context = EmitterContext(currentAnalysisContext(), configuration, emitter) - // We need to apply the tracing emitters before the non-tracing ones. - Processor.processEntriesOfType(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) { + Processor.processEntriesOfType(emitters, analysisContext.messages) { it.emit(context, instruction) } if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt index f904d276b7..2eb5e0de5d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt @@ -18,10 +18,10 @@ interface Emitter { fun emit(context: EmitterContext, instruction: Instruction) /** - * Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox. + * Determines the order in which emitters are executed within the sandbox. */ @JvmDefault - val isTracer: Boolean - get() = false + val priority: Int + get() = EMIT_DEFAULT } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt index 2e2d2fc2e4..e51647830b 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt @@ -168,6 +168,14 @@ class EmitterModule( inline fun 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. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt index 93a9c5bf7d..3d3b86d2af 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt @@ -2,11 +2,22 @@ package net.corda.djvm.code import org.objectweb.asm.Type +import sandbox.java.lang.DJVMException import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.rules.RuleViolationError +/** + * These are the priorities for executing [Emitter] instances. + * Tracing emitters are executed first. + */ +const val EMIT_TRACING: Int = 0 +const val EMIT_TRAPPING_EXCEPTIONS: Int = EMIT_TRACING + 1 +const val EMIT_HANDLING_EXCEPTIONS: Int = EMIT_TRAPPING_EXCEPTIONS + 1 +const val EMIT_DEFAULT: Int = 10 + val ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java) val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::class.java) +val djvmException: String = Type.getInternalName(DJVMException::class.java) /** * Local extension method for normalizing a class name. diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt new file mode 100644 index 0000000000..a984766d29 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt @@ -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() \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt index ac9b9e643f..943d745c80 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt @@ -9,6 +9,6 @@ import org.objectweb.asm.Label * @property handler The label of the exception handler. */ class TryCatchBlock( - val typeName: String, - val handler: Label -) : NoOperationInstruction() + typeName: String, + handler: Label +) : TryBlock(handler, typeName) diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt index 808575b05d..7ec2149b73 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt @@ -7,7 +7,6 @@ import org.objectweb.asm.Label * * @property handler The handler for the finally-block. */ -@Suppress("MemberVisibilityCanBePrivate") class TryFinallyBlock( - val handler: Label -) : NoOperationInstruction() + handler: Label +) : TryBlock(handler, "") diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt index 081bff4fa5..4804074457 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt @@ -1,7 +1,6 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration -import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import net.corda.djvm.code.ClassMutator @@ -11,6 +10,8 @@ import net.corda.djvm.references.Member import net.corda.djvm.utilities.loggerFor import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor /** * Functionality for rewriting parts of a class as it is being loaded. @@ -22,6 +23,7 @@ open class ClassRewriter( private val configuration: SandboxConfiguration, private val classLoader: ClassLoader ) { + private val analysisConfig = configuration.analysisConfiguration /** * Process class and allow user to rewrite parts/all of its content through provided hooks. @@ -32,13 +34,15 @@ open class ClassRewriter( fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode { logger.debug("Rewriting class {}...", reader.className) val writer = SandboxClassWriter(reader, classLoader) - val analysisConfiguration = configuration.analysisConfiguration - val classRemapper = SandboxClassRemapper(InterfaceStitcher(writer, analysisConfiguration), analysisConfiguration) + val classRemapper = SandboxClassRemapper( + ClassExceptionRemapper(SandboxStitcher(writer)), + analysisConfig + ) val visitor = ClassMutator( - classRemapper, - analysisConfiguration, - configuration.definitionProviders, - configuration.emitters + classRemapper, + analysisConfig, + configuration.definitionProviders, + configuration.emitters ) visitor.analyze(reader, context, options = ClassReader.EXPAND_FRAMES) return ByteCode(writer.toByteArray(), visitor.hasBeenModified) @@ -50,25 +54,30 @@ open class ClassRewriter( /** * Extra visitor that is applied after [SandboxRemapper]. This "stitches" the original - * unmapped interface as a super-interface of the mapped version. + * unmapped interface as a super-interface of the mapped version, as well as adding + * any extra methods that are needed. */ - private class InterfaceStitcher(parent: ClassVisitor, private val configuration: AnalysisConfiguration) + private inner class SandboxStitcher(parent: ClassVisitor) : ClassVisitor(API_VERSION, parent) { private val extraMethods = mutableListOf() override fun visit(version: Int, access: Int, className: String, signature: String?, superName: String?, interfaces: Array?) { - val stitchedInterfaces = configuration.stitchedInterfaces[className]?.let { methods -> + val stitchedInterfaces = analysisConfig.stitchedInterfaces[className]?.let { methods -> extraMethods += methods - arrayOf(*(interfaces ?: emptyArray()), configuration.classResolver.reverse(className)) + arrayOf(*(interfaces ?: emptyArray()), analysisConfig.classResolver.reverse(className)) } ?: interfaces + analysisConfig.stitchedClasses[className]?.also { methods -> + extraMethods += methods + } + super.visit(version, access, className, signature, superName, stitchedInterfaces) } override fun visitEnd() { for (method in extraMethods) { - method.apply { + with(method) { visitMethod(access, memberName, signature, genericsDetails.emptyAsNull, exceptions.toTypedArray())?.also { mv -> mv.visitCode() EmitterModule(mv).writeByteCode(body) @@ -81,4 +90,26 @@ open class ClassRewriter( super.visitEnd() } } + + /** + * Map exceptions in method signatures to their sandboxed equivalents. + */ + private inner class ClassExceptionRemapper(parent: ClassVisitor) : ClassVisitor(API_VERSION, parent) { + override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array?): 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) + } + } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt index 7f2abccb6a..4dbeae7ab2 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt @@ -3,6 +3,9 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException +import net.corda.djvm.code.asPackagePath import net.corda.djvm.code.asResourcePath import net.corda.djvm.references.ClassReference import net.corda.djvm.source.ClassSource @@ -33,7 +36,7 @@ class SandboxClassLoader( /** * The analyzer used to traverse the class hierarchy. */ - val analyzer: ClassAndMemberVisitor + private val analyzer: ClassAndMemberVisitor get() = ruleValidator /** @@ -56,6 +59,18 @@ class SandboxClassLoader( */ private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader) + /** + * We need to load this class up front, so that we can identify sandboxed exception classes. + */ + private val throwableClass: Class<*> + + init { + // Bootstrap the loading of the sandboxed Throwable class. + loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Object"), context) + loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.StackTraceElement"), context) + throwableClass = loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Throwable"), context).type + } + /** * Given a class name, provide its corresponding [LoadedClass] for the sandbox. */ @@ -77,11 +92,43 @@ class SandboxClassLoader( */ @Throws(ClassNotFoundException::class) override fun loadClass(name: String, resolve: Boolean): Class<*> { - val source = ClassSource.fromClassName(name) - return if (name.startsWith("sandbox.") && !analysisConfiguration.isPinnedClass(source.internalClassName)) { - loadClassAndBytes(source, context).type + var clazz = findLoadedClass(name) + if (clazz == null) { + 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 { - 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. - val clazz = try { + val clazz: Class<*> = try { when { whitelistedClasses.matches(sourceName.asResourcePath) -> supportingClassLoader.loadClass(sourceName) else -> defineClass(resolvedName, byteCode.bytes, 0, byteCode.bytes.size) @@ -167,6 +214,15 @@ class SandboxClassLoader( } } + private fun loadWrapperFor(throwable: Class<*>): LoadedClass { + val className = analysisConfiguration.exceptionResolver.getThrowableName(throwable) + return loadedClasses.getOrPut(className) { + val superName = analysisConfiguration.exceptionResolver.getThrowableSuperName(throwable) + val byteCode = ThrowableWrapperFactory.toByteCode(className, superName) + LoadedClass(defineClass(className.asPackagePath, byteCode.bytes, 0, byteCode.bytes.size), byteCode) + } + } + private companion object { private val logger = loggerFor() private val UNMODIFIED = ByteCode(ByteArray(0), false) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt index 7412999727..9f90981f57 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt @@ -3,7 +3,6 @@ package net.corda.djvm.rewiring import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import org.objectweb.asm.ClassVisitor -import org.objectweb.asm.Label import org.objectweb.asm.MethodVisitor import org.objectweb.asm.commons.ClassRemapper @@ -35,11 +34,6 @@ class SandboxClassRemapper(cv: ClassVisitor, private val configuration: Analysis return mapperFor(method).visitMethodInsn(opcode, owner, name, descriptor, isInterface) } - override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, type: String?) { - // Don't map caught exception names - these could be thrown by the JVM itself. - nonmapper.visitTryCatchBlock(start, end, handler, type) - } - override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { val field = Element(owner, name, descriptor) return mapperFor(field).visitFieldInsn(opcode, owner, name, descriptor) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt new file mode 100644 index 0000000000..3557ed50cf --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt @@ -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. + * + * + * 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; + * } + * } + * + */ + 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, "", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitMethodInsn(INVOKESPECIAL, superName, "", "()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. + * + * + * public class CLASSNAME extends SUPERNAME { + * public CLASSNAME(sandbox.java.lang.Throwable t) { + * super(t); + * } + * } + * + */ + private fun ClassWriter.childClass() { + // Class definition + visit( + V1_8, + ACC_SYNTHETIC or ACC_PUBLIC, + className, + null, + superName, + arrayOf() + ) + + // Constructor + visitMethod(ACC_PUBLIC, "", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitVarInsn(ALOAD, 1) + mv.visitMethodInsn(INVOKESPECIAL, superName, "", CONSTRUCTOR_DESCRIPTOR, false) + mv.visitInsn(RETURN) + mv.visitMaxs(2, 2) + mv.visitEnd() + } + + // End of class + visitEnd() + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt index a5524ec12b..d898a747b0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt @@ -27,30 +27,36 @@ class DisallowCatchingBlacklistedExceptions : Emitter { companion object { private val disallowedExceptionTypes = setOf( - ruleViolationError, - thresholdViolationError, + ruleViolationError, + thresholdViolationError, - /** - * These errors indicate that the JVM is failing, - * so don't allow these to be caught either. - */ - "java/lang/StackOverflowError", - "java/lang/OutOfMemoryError", + /** + * These errors indicate that the JVM is failing, + * so don't allow these to be caught either. + */ + "java/lang/StackOverflowError", + "java/lang/OutOfMemoryError", - /** - * These are immediate super-classes for our explicit errors. - */ - "java/lang/VirtualMachineError", - "java/lang/ThreadDeath", + /** + * These are immediate super-classes for our explicit errors. + */ + "java/lang/VirtualMachineError", + "java/lang/ThreadDeath", - /** - * Any of [ThreadDeath] and [VirtualMachineError]'s throwable - * super-classes also need explicit checking. - */ - "java/lang/Throwable", - "java/lang/Error" + /** + * Any of [ThreadDeath] and [VirtualMachineError]'s throwable + * super-classes also need explicit checking. + */ + "java/lang/Throwable", + "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 } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt new file mode 100644 index 0000000000..b616128b85 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt @@ -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() + + 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 +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt new file mode 100644 index 0000000000..037b4012b0 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt @@ -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;") + } + } + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt index 839ac609bc..a8577c19ac 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -40,7 +41,7 @@ class TraceAllocations : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt index b71f1f4657..cafafba1ea 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceInvocations : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt index 1d7695380b..ce4e41eaa8 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceJumps : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt index b4756b272e..dc8064ff15 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceThrows : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt index 99ef5319fb..4cb15b9194 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt @@ -21,12 +21,14 @@ class ClassSource private constructor( /** * Instantiate a [ClassSource] from a fully qualified class name. */ + @JvmStatic fun fromClassName(className: String, origin: String? = null) = ClassSource(className, origin) /** * Instantiate a [ClassSource] from a file on disk. */ + @JvmStatic fun fromPath(path: Path) = PathClassSource(path) /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt index 8b4789f8df..fbc0f1b0f0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt @@ -2,6 +2,8 @@ package net.corda.djvm.source import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassResolver +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException import net.corda.djvm.analysis.SourceLocation import net.corda.djvm.code.asResourcePath import net.corda.djvm.messages.Message @@ -61,7 +63,15 @@ abstract class AbstractSourceClassLoader( */ override fun loadClass(name: String, resolve: Boolean): Class<*> { logger.trace("Loading class {}, resolve={}...", name, resolve) - val originalName = classResolver.reverseNormalized(name) + val originalName = classResolver.reverseNormalized(name).let { n -> + // A synthetic exception should be mapped back to its + // corresponding exception in the original hierarchy. + if (isDJVMException(n)) { + getDJVMExceptionOwner(n) + } else { + n + } + } return super.loadClass(originalName, resolve) } diff --git a/djvm/src/main/kotlin/sandbox/Task.kt b/djvm/src/main/kotlin/sandbox/Task.kt index 8a2bbab78a..0be04225bf 100644 --- a/djvm/src/main/kotlin/sandbox/Task.kt +++ b/djvm/src/main/kotlin/sandbox/Task.kt @@ -6,7 +6,10 @@ import sandbox.java.lang.unsandbox typealias SandboxFunction = sandbox.java.util.function.Function -@Suppress("unused") +internal fun isEntryPoint(elt: java.lang.StackTraceElement): Boolean { + return elt.className == "sandbox.Task" && elt.methodName == "apply" +} + class Task(private val function: SandboxFunction?) : SandboxFunction { /** diff --git a/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt index b6a3acdc77..a098d78020 100644 --- a/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt +++ b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt @@ -2,19 +2,25 @@ @file:Suppress("unused") package sandbox.java.lang +import net.corda.djvm.analysis.AnalysisConfiguration.Companion.JVM_EXCEPTIONS +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMException +import net.corda.djvm.rules.implementation.* import org.objectweb.asm.Opcodes.ACC_ENUM +import org.objectweb.asm.Type +import sandbox.isEntryPoint +import sandbox.net.corda.djvm.rules.RuleViolationError private const val SANDBOX_PREFIX = "sandbox." fun Any.unsandbox(): Any { return when (this) { - is Enum<*> -> fromDJVMEnum() is Object -> fromDJVM() is Array<*> -> fromDJVMArray() else -> this } } +@Throws(ClassNotFoundException::class) fun Any.sandbox(): Any { return when (this) { is kotlin.String -> String.toDJVM(this) @@ -27,6 +33,7 @@ fun Any.sandbox(): Any { is kotlin.Double -> Double.toDJVM(this) is kotlin.Boolean -> Boolean.toDJVM(this) is kotlin.Enum<*> -> toDJVMEnum() + is kotlin.Throwable -> toDJVMThrowable() is Array<*> -> toDJVMArray() else -> this } @@ -38,8 +45,11 @@ private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this) * These functions use the "current" classloader, i.e. classloader * that owns this DJVM class. */ -private fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) -private fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) +@Throws(ClassNotFoundException::class) +internal fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) + +@Throws(ClassNotFoundException::class) +internal fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) private fun kotlin.String.toSandboxPackage(): kotlin.String { return if (startsWith(SANDBOX_PREFIX)) { @@ -66,10 +76,12 @@ private inline fun Array<*>.toDJVMArray(): Array { } } -private fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> { +@Throws(ClassNotFoundException::class) +internal fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> { return javaClass.fromDJVMType().enumConstants[ordinal()] as kotlin.Enum<*> } +@Throws(ClassNotFoundException::class) private fun kotlin.Enum<*>.toDJVMEnum(): Enum<*> { @Suppress("unchecked_cast") return (getEnumConstants(javaClass.toDJVMType() as Class>) as Array>)[ordinal] @@ -87,10 +99,11 @@ fun getEnumConstants(clazz: Class>): Array<*>? { internal fun enumConstantDirectory(clazz: Class>): sandbox.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) } -@Suppress("unchecked_cast") +@Suppress("unchecked_cast", "ReplaceGetOrSet") internal fun getEnumConstantsShared(clazz: Class>): Array>? { return if (isEnum(clazz)) { // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. @@ -100,7 +113,7 @@ internal fun getEnumConstantsShared(clazz: Class>): Array>): Array>? { return clazz.getMethod("values").let { method -> method.isAccessible = true @@ -109,6 +122,7 @@ private fun createEnum(clazz: Class>): Array>? { }?.apply { allEnums.put(clazz, this) } } +@Suppress("ReplacePutWithAssignment") private fun createEnumDirectory(clazz: Class>): sandbox.java.util.Map> { val universe = getEnumConstantsShared(clazz) ?: throw IllegalArgumentException("${clazz.name} is not an enum type") val directory = sandbox.java.util.LinkedHashMap>(2 * universe.size) @@ -154,5 +168,130 @@ private fun toSandbox(className: kotlin.String): kotlin.String { private val bannedClasses = setOf( "^java\\.lang\\.DJVM(.*)?\$".toRegex(), "^net\\.corda\\.djvm\\..*\$".toRegex(), - "^Task\$".toRegex() + "^Task(.*)?\$".toRegex() ) + +/** + * Exception Management. + * + * This function converts a [sandbox.java.lang.Throwable] into a + * [java.lang.Throwable] that the JVM can actually throw. + */ +fun fromDJVM(t: Throwable?): kotlin.Throwable { + return if (t is DJVMThrowableWrapper) { + // We must be exiting a finally block. + t.fromDJVM() + } else { + try { + /** + * Someone has created a [sandbox.java.lang.Throwable] + * and is (re?)throwing it. + */ + val sandboxedName = t!!.javaClass.name + if (Type.getInternalName(t.javaClass) in JVM_EXCEPTIONS) { + // We map these exceptions to their equivalent JVM classes. + Class.forName(sandboxedName.fromSandboxPackage()).createJavaThrowable(t) + } else { + // Whereas the sandbox creates a synthetic throwable wrapper for these. + Class.forName(getDJVMException(sandboxedName)) + .getDeclaredConstructor(sandboxThrowable) + .newInstance(t) as kotlin.Throwable + } + } catch (e: Exception) { + RuleViolationError(e.message) + } + } +} + +/** + * Wraps a [java.lang.Throwable] inside a [sandbox.java.lang.Throwable]. + * This function is invoked at the beginning of a finally block, and + * so does not need to return a reference to the equivalent sandboxed + * exception. The finally block only needs to be able to re-throw the + * original exception when it finishes. + */ +fun finally(t: kotlin.Throwable): Throwable = DJVMThrowableWrapper(t) + +/** + * Converts a [java.lang.Throwable] into a [sandbox.java.lang.Throwable]. + * It is invoked at the start of each catch block. + * + * Note: [DisallowCatchingBlacklistedExceptions] means that we don't + * need to handle [ThreadDeath] here. + */ +fun catch(t: kotlin.Throwable): Throwable { + try { + return t.toDJVMThrowable() + } catch (e: Exception) { + throw RuleViolationError(e.message) + } +} + +/** + * Worker functions to convert [java.lang.Throwable] into [sandbox.java.lang.Throwable]. + */ +private fun kotlin.Throwable.toDJVMThrowable(): Throwable { + return (this as? DJVMException)?.getThrowable() ?: javaClass.toDJVMType().createDJVMThrowable(this) +} + +/** + * Creates a new [sandbox.java.lang.Throwable] from a [java.lang.Throwable], + * which was probably thrown by the JVM itself. + */ +private fun Class<*>.createDJVMThrowable(t: kotlin.Throwable): Throwable { + return (try { + getDeclaredConstructor(String::class.java).newInstance(String.toDJVM(t.message)) + } catch (e: NoSuchMethodException) { + newInstance() + } as Throwable).apply { + t.cause?.also { + initCause(it.toDJVMThrowable()) + } + stackTrace = sanitiseToDJVM(t.stackTrace) + } +} + +private fun Class<*>.createJavaThrowable(t: Throwable): kotlin.Throwable { + return (try { + getDeclaredConstructor(kotlin.String::class.java).newInstance(String.fromDJVM(t.message)) + } catch (e: NoSuchMethodException) { + newInstance() + } as kotlin.Throwable).apply { + t.cause?.also { + initCause(fromDJVM(it)) + } + stackTrace = copyFromDJVM(t.stackTrace) + } +} + +private fun sanitiseToDJVM(source: Array): Array { + var idx = 0 + while (idx < source.size && !isEntryPoint(source[idx])) { + ++idx + } + return copyToDJVM(source, 0, idx) +} + +internal fun copyToDJVM(source: Array, fromIdx: Int, toIdx: Int): Array { + 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): Array { + 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 diff --git a/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt b/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt new file mode 100644 index 0000000000..553e8533ab --- /dev/null +++ b/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt @@ -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 +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt index 0fe4283caf..b312a4091f 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt @@ -6,4 +6,4 @@ package sandbox.net.corda.djvm.costing * * @property message The description of the condition causing the problem. */ -class ThresholdViolationError(override val message: String) : ThreadDeath() +class ThresholdViolationError(override val message: String?) : ThreadDeath() diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt index 24b0e73775..1bd39bdf39 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt @@ -7,4 +7,4 @@ package sandbox.net.corda.djvm.rules * * @property message The description of the condition causing the problem. */ -class RuleViolationError(override val message: String) : ThreadDeath() \ No newline at end of file +class RuleViolationError(override val message: String?) : ThreadDeath() \ No newline at end of file diff --git a/djvm/src/test/java/net/corda/djvm/WithJava.java b/djvm/src/test/java/net/corda/djvm/WithJava.java new file mode 100644 index 0000000000..3e8cf05145 --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/WithJava.java @@ -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 ExecutionSummaryWithResult run( + SandboxExecutor executor, Class> 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); + } + } + } + +} diff --git a/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java b/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java new file mode 100644 index 0000000000..0343d9517d --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java @@ -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 executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult 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 executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult 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 executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult 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 executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult 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 executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, UseEnumSet.class, ExampleEnum.ONE); + assertThat(output.getResult()).isTrue(); + return null; + }); + } + + public static class AssertEnum implements Function { + @Override + public Boolean apply(ExampleEnum input) { + return input.getClass().isEnum(); + } + } + + public static class TransformEnum implements Function { + @Override + public String[] apply(Integer input) { + return Stream.of(ExampleEnum.values()).map(ExampleEnum::name).toArray(String[]::new); + } + } + + public static class FetchEnum implements Function { + public ExampleEnum apply(String input) { + return ExampleEnum.valueOf(input); + } + } + + public static class UseEnumMap implements Function { + @Override + public Integer apply(ExampleEnum input) { + Map map = new EnumMap<>(ExampleEnum.class); + map.put(input, input.name()); + return map.size(); + } + } + + public static class UseEnumSet implements Function { + @Override + public Boolean apply(ExampleEnum input) { + return EnumSet.allOf(ExampleEnum.class).contains(input); + } + } +} diff --git a/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java b/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java new file mode 100644 index 0000000000..26203f641b --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java @@ -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 executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult 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 executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + + ExecutionSummaryWithResult success = WithJava.run(executor, JavaWithCheckedExceptions.class, "http://localhost:8080/hello/world"); + assertThat(success.getResult()).isEqualTo("/hello/world"); + + ExecutionSummaryWithResult 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 { + @Override + public String[] apply(String input) { + List 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 { + @Override + public String apply(String input) { + try { + return new URI(input).getPath(); + } catch (URISyntaxException e) { + return "CATCH:" + e.getMessage(); + } + } + } +} diff --git a/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt b/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt new file mode 100644 index 0000000000..886b1efacc --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt @@ -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 { + override fun apply(input: Any?): sandbox.java.lang.Throwable? { + return sandbox.java.lang.Throwable(input as? sandbox.java.lang.String) + } +} + +class MultipleExceptionsTask : SandboxFunction { + 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.toLineNumbers(): IntArray { + return map(StackTraceElement::getLineNumber).toIntArray() +} \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt index d71fa1a36a..37048ee7f2 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt @@ -113,14 +113,4 @@ class DJVMTest { assertArrayEquals(ByteArray(1) { 127.toByte() }, result[9] as ByteArray) assertArrayEquals(CharArray(1) { '?' }, result[10] as CharArray) } - - private fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this) - private fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this) - private fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this) - private fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this) - private fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this) - private fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this) - private fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this) - private fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this) - private fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this) } \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt index a771798655..ad16eee53a 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt @@ -37,22 +37,29 @@ abstract class TestBase { val ALL_EMITTERS = Discovery.find() // We need at least these emitters to handle the Java API classes. + @JvmField val BASIC_EMITTERS: List = listOf( ArgumentUnwrapper(), + HandleExceptionUnwrapper(), ReturnTypeWrapper(), RewriteClassMethods(), - StringConstantWrapper() + StringConstantWrapper(), + ThrowExceptionWrapper() ) val ALL_DEFINITION_PROVIDERS = Discovery.find() // We need at least these providers to handle the Java API classes. + @JvmField val BASIC_DEFINITION_PROVIDERS: List = listOf(StaticConstantRemover()) + @JvmField val BLANK = emptySet() + @JvmField val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass) + @JvmField val DETERMINISTIC_RT: Path = Paths.get( System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set")) @@ -89,7 +96,7 @@ abstract class TestBase { val reader = ClassReader(T::class.java.name) AnalysisConfiguration( minimumSeverityLevel = minimumSeverityLevel, - classPath = listOf(DETERMINISTIC_RT) + bootstrapJar = DETERMINISTIC_RT ).use { analysisConfiguration -> val validator = RuleValidator(ALL_RULES, analysisConfiguration) val context = AnalysisContext.fromConfiguration(analysisConfiguration) diff --git a/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt index d493238723..6313661b0c 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt @@ -1,22 +1,24 @@ +@file:JvmName("UtilityFunctions") package net.corda.djvm import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.rules.RuleViolationError +/** + * Allows us to create a [Utilities] object that we can pin inside the sandbox. + */ object Utilities { fun throwRuleViolationError(): Nothing = throw RuleViolationError("Can't catch this!") fun throwThresholdViolationError(): Nothing = throw ThresholdViolationError("Can't catch this!") - - fun throwContractConstraintViolation(): Nothing = throw IllegalArgumentException("Contract constraint violated") - - fun throwError(): Nothing = throw Error() - - fun throwThrowable(): Nothing = throw Throwable() - - fun throwThreadDeath(): Nothing = throw ThreadDeath() - - fun throwStackOverflowError(): Nothing = throw StackOverflowError("FAKE OVERFLOW!") - - fun throwOutOfMemoryError(): Nothing = throw OutOfMemoryError("FAKE OOM!") } + +fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this) +fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this) +fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this) +fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this) +fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this) +fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this) +fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this) +fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this) +fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this) \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt index a3919c964c..32fa876195 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt @@ -6,14 +6,8 @@ import foo.bar.sandbox.toNumber import net.corda.djvm.TestBase import net.corda.djvm.analysis.Whitelist import net.corda.djvm.Utilities -import net.corda.djvm.Utilities.throwContractConstraintViolation -import net.corda.djvm.Utilities.throwError -import net.corda.djvm.Utilities.throwOutOfMemoryError import net.corda.djvm.Utilities.throwRuleViolationError -import net.corda.djvm.Utilities.throwStackOverflowError -import net.corda.djvm.Utilities.throwThreadDeath import net.corda.djvm.Utilities.throwThresholdViolationError -import net.corda.djvm.Utilities.throwThrowable import net.corda.djvm.assertions.AssertionExtensions.withProblem import net.corda.djvm.rewiring.SandboxClassLoadingException import org.assertj.core.api.Assertions.assertThat @@ -55,7 +49,7 @@ class SandboxExecutorTest : TestBase() { class Contract : Function { override fun apply(input: Transaction) { - throwContractConstraintViolation() + throw IllegalArgumentException("Contract constraint violated") } } @@ -74,11 +68,7 @@ class SandboxExecutorTest : TestBase() { val obj = Object() val hash1 = obj.hashCode() val hash2 = obj.hashCode() - //require(hash1 == hash2) - // TODO: Replace require() once we have working exception support. - if (hash1 != hash2) { - throwError() - } + require(hash1 == hash2) return Object().hashCode() } } @@ -180,7 +170,7 @@ class SandboxExecutorTest : TestBase() { class TestCatchThreadDeath : Function { override fun apply(input: Int): Int { return try { - throwThreadDeath() + throw ThreadDeath() } catch (exception: ThreadDeath) { 1 } @@ -261,8 +251,8 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throwThrowable() - 2 -> throwError() + 1 -> throw Throwable() + 2 -> throw Error() else -> 0 } } catch (exception: Error) { @@ -277,20 +267,20 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throwThrowable() - 2 -> throwError() + 1 -> throw Throwable() + 2 -> throw Error() 3 -> try { - throwThreadDeath() + throw ThreadDeath() } catch (ex: ThreadDeath) { 3 } 4 -> try { - throwStackOverflowError() + throw StackOverflowError("FAKE OVERFLOW!") } catch (ex: StackOverflowError) { 4 } 5 -> try { - throwOutOfMemoryError() + throw OutOfMemoryError("FAKE OOM!") } catch (ex: OutOfMemoryError) { 5 } diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt new file mode 100644 index 0000000000..ae013a9c1e --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt @@ -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>(configuration) + contractExecutor.run("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>(configuration) + contractExecutor.run("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(configuration) + contractExecutor.run(-1).apply { + assertThat(result) + .isEqualTo("sandbox.java.lang.ArrayIndexOutOfBoundsException:-1") + } + } +} + +class ThrowAndRethrowExample : Function> { + override fun apply(input: String): Array { + val data = mutableListOf() + 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> { + override fun apply(input: String): Array { + val data = mutableListOf() + 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 { + override fun apply(input: Int): String { + return try { + arrayOf(0, 1, 2)[input] + "No Error" + } catch (e: Exception) { + e.javaClass.name + ':' + (e.message ?: "") + } + } +} + +open class MyBaseException(message: String) : Exception(message) +class MyExampleException(message: String) : MyBaseException(message) \ No newline at end of file