ENT-1906: Don't transform java.Object[] to sandbox.Object[]. (#4115)

* Don't transform java.Object[] to sandbox.Object[] because an array is still java.Object inside the sandbox.
* Add extra test for sandboxing an array of int[].
* Ensure hashCode() is deterministic for Java arrays.
* Remove some information about DJVM from some exception stack traces.
This commit is contained in:
Chris Rankin 2018-10-29 16:32:40 +00:00 committed by GitHub
parent 34cadc4264
commit ff9061b968
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 200 additions and 13 deletions

View File

@ -24,8 +24,12 @@ class AlwaysInheritFromSandboxedObject : ClassDefinitionProvider, Emitter {
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
if (instruction is TypeInstruction &&
instruction.typeName == OBJECT_NAME) {
instruction.typeName == OBJECT_NAME &&
instruction.operation != Opcodes.ANEWARRAY &&
instruction.operation != Opcodes.MULTIANEWARRAY) {
// When creating new objects, make sure the sandboxed type gets used.
// However, an array is always [java.lang.Object] so we must exclude
// arrays from this so that we can still support arrays of arrays.
new(SANDBOX_OBJECT_NAME, instruction.operation)
preventDefault()
}

View File

@ -0,0 +1,29 @@
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 net.corda.djvm.code.instructions.MemberAccessInstruction
import org.objectweb.asm.Opcodes.*
/**
* We cannot wrap Java array objects - and potentially others - and so these would still
* use the non-deterministic [java.lang.Object.hashCode] by default. Therefore we intercept
* these invocations and redirect them to our [sandbox.java.lang.DJVM] object.
*/
class RewriteObjectMethods : Emitter {
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
if (instruction is MemberAccessInstruction && instruction.owner == "java/lang/Object") {
when (instruction.operation) {
INVOKEVIRTUAL -> if (instruction.memberName == "hashCode" && instruction.signature == "()I") {
invokeStatic(
owner = "sandbox/java/lang/DJVM",
name = "hashCode",
descriptor = "(Ljava/lang/Object;)I"
)
preventDefault()
}
}
}
}
}

View File

@ -43,14 +43,15 @@ fun Any.sandbox(): Any {
private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this)
/**
* Use the sandbox's classloader explicitly, because this class
* might belong to the shared parent classloader.
* Use [Class.forName] so that we can also fetch classes for arrays of primitive types.
* Also use the sandbox's classloader explicitly here, because this invoking class
* might belong to a shared parent classloader.
*/
@Throws(ClassNotFoundException::class)
internal fun Class<*>.toDJVMType(): Class<*> = SandboxRuntimeContext.instance.classLoader.loadClass(name.toSandboxPackage())
internal fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage(), false, SandboxRuntimeContext.instance.classLoader)
@Throws(ClassNotFoundException::class)
internal fun Class<*>.fromDJVMType(): Class<*> = SandboxRuntimeContext.instance.classLoader.loadClass(name.fromSandboxPackage())
internal fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage(), false, SandboxRuntimeContext.instance.classLoader)
private fun kotlin.String.toSandboxPackage(): kotlin.String {
return if (startsWith(SANDBOX_PREFIX)) {
@ -139,6 +140,21 @@ private fun createEnumDirectory(clazz: Class<out Enum<*>>): sandbox.java.util.Ma
private val allEnums: sandbox.java.util.Map<Class<out Enum<*>>, Array<out Enum<*>>> = sandbox.java.util.LinkedHashMap()
private val allEnumDirectories: sandbox.java.util.Map<Class<out Enum<*>>, sandbox.java.util.Map<String, out Enum<*>>> = sandbox.java.util.LinkedHashMap()
/**
* Replacement function for Object.hashCode(), because some objects
* (i.e. arrays) cannot be replaced by [sandbox.java.lang.Object].
*/
fun hashCode(obj: Any?): Int {
return if (obj is Object) {
obj.hashCode()
} else if (obj != null) {
System.identityHashCode(obj)
} else {
// Throw the same exception that the JVM would throw in this case.
throw NullPointerException().sanitise()
}
}
/**
* Replacement functions for Class<*>.forName(...) which protect
* against users loading classes from outside the sandbox.
@ -161,7 +177,7 @@ fun classForName(className: kotlin.String, initialize: kotlin.Boolean, classLoad
*/
private fun toSandbox(className: kotlin.String): kotlin.String {
if (bannedClasses.any { it.matches(className) }) {
throw ClassNotFoundException(className)
throw ClassNotFoundException(className).sanitise()
}
return SANDBOX_PREFIX + className
}
@ -200,7 +216,7 @@ fun fromDJVM(t: Throwable?): kotlin.Throwable {
.newInstance(t) as kotlin.Throwable
}
} catch (e: Exception) {
RuleViolationError(e.message)
RuleViolationError(e.message).sanitise()
}
}
}
@ -225,10 +241,20 @@ fun catch(t: kotlin.Throwable): Throwable {
try {
return t.toDJVMThrowable()
} catch (e: Exception) {
throw RuleViolationError(e.message)
throw RuleViolationError(e.message).sanitise()
}
}
/**
* Clean up exception stack trace for throwing.
*/
private fun <T: kotlin.Throwable> T.sanitise(): T {
stackTrace = stackTrace.let {
it.sliceArray(1 until findEntryPointIndex(it))
}
return this
}
/**
* Worker functions to convert [java.lang.Throwable] into [sandbox.java.lang.Throwable].
*/
@ -266,12 +292,16 @@ private fun Class<*>.createJavaThrowable(t: Throwable): kotlin.Throwable {
}
}
private fun sanitiseToDJVM(source: Array<java.lang.StackTraceElement>): Array<StackTraceElement> {
private fun findEntryPointIndex(source: Array<java.lang.StackTraceElement>): Int {
var idx = 0
while (idx < source.size && !isEntryPoint(source[idx])) {
++idx
}
return copyToDJVM(source, 0, idx)
return idx
}
private fun sanitiseToDJVM(source: Array<java.lang.StackTraceElement>): Array<StackTraceElement> {
return copyToDJVM(source, 0, findEntryPointIndex(source))
}
internal fun copyToDJVM(source: Array<java.lang.StackTraceElement>, fromIdx: Int, toIdx: Int): Array<StackTraceElement> {

View File

@ -1,6 +1,7 @@
package net.corda.djvm;
import net.corda.djvm.execution.ExecutionSummaryWithResult;
import net.corda.djvm.execution.SandboxException;
import net.corda.djvm.execution.SandboxExecutor;
import net.corda.djvm.source.ClassSource;
@ -13,12 +14,15 @@ public interface WithJava {
try {
return executor.run(ClassSource.fromClassName(task.getName(), null), input);
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
if (e instanceof SandboxException) {
throw asRuntime(e.getCause());
} else {
throw new RuntimeException(e.getMessage(), e);
throw asRuntime(e);
}
}
}
static RuntimeException asRuntime(Throwable t) {
return (t instanceof RuntimeException) ? (RuntimeException) t : new RuntimeException(t.getMessage(), t);
}
}

View File

@ -0,0 +1,91 @@
package net.corda.djvm.execution;
import net.corda.djvm.TestBase;
import net.corda.djvm.WithJava;
import org.junit.Test;
import java.util.function.Function;
import static net.corda.djvm.messages.Severity.WARNING;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class SandboxObjectHashCodeJavaTest extends TestBase {
@Test
public void testHashForArray() {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<Object, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<Integer> output = WithJava.run(executor, ArrayHashCode.class, null);
assertThat(output.getResult()).isEqualTo(0xfed_c0de + 1);
return null;
});
}
@Test
public void testHashForObjectInArray() {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<Object, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<Integer> output = WithJava.run(executor, ObjectInArrayHashCode.class, null);
assertThat(output.getResult()).isEqualTo(0xfed_c0de + 1);
return null;
});
}
@Test
public void testHashForNullObject() {
assertThatExceptionOfType(NullPointerException.class)
.isThrownBy(() -> new HashCode().apply(null));
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<Object, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
assertThatExceptionOfType(NullPointerException.class)
.isThrownBy(() -> WithJava.run(executor, HashCode.class, null));
return null;
});
}
@Test
public void testHashForWrappedInteger() {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<Object, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<Integer> output = WithJava.run(executor, HashCode.class, 1234);
assertThat(output.getResult()).isEqualTo(Integer.hashCode(1234));
return null;
});
}
@Test
public void testHashForWrappedString() {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<Object, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<Integer> output = WithJava.run(executor, HashCode.class, "Burble");
assertThat(output.getResult()).isEqualTo("Burble".hashCode());
return null;
});
}
public static class ObjectInArrayHashCode implements Function<Object, Integer> {
@Override
public Integer apply(Object obj) {
Object[] arr = new Object[1];
arr[0] = new Object();
return arr[0].hashCode();
}
}
public static class ArrayHashCode implements Function<Object, Integer> {
@SuppressWarnings("all")
@Override
public Integer apply(Object obj) {
return new Object[0].hashCode();
}
}
public static class HashCode implements Function<Object, Integer> {
@Override
public Integer apply(Object obj) {
return obj.hashCode();
}
}
}

View File

@ -49,6 +49,7 @@ abstract class TestBase {
HandleExceptionUnwrapper(),
ReturnTypeWrapper(),
RewriteClassMethods(),
RewriteObjectMethods(),
StringConstantWrapper(),
ThrowExceptionWrapper()
)

View File

@ -733,4 +733,32 @@ class SandboxExecutorTest : TestBase() {
return cl.find()
}
}
@Test
fun `test creating arrays of arrays`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Any, Array<Any>>(configuration)
contractExecutor.run<ArraysOfArrays>("THINGY").apply {
assertThat(result).isEqualTo(arrayOf(arrayOf("THINGY")))
}
}
class ArraysOfArrays : Function<Any, Array<Any>> {
override fun apply(input: Any): Array<Any> {
return arrayOf(arrayOf(input))
}
}
@Test
fun `test creating arrays of int arrays`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Array<IntArray>>(configuration)
contractExecutor.run<ArrayOfIntArrays>(0).apply {
assertThat(result).isEqualTo(arrayOf(intArrayOf(0)))
}
}
class ArrayOfIntArrays : Function<Int, Array<IntArray>> {
override fun apply(input: Int): Array<IntArray> {
return arrayOf(intArrayOf(input))
}
}
}