mirror of
https://github.com/corda/corda.git
synced 2025-02-21 01:42:24 +00:00
Import sandbox code developed by Ben Evans with review and contributions from myself.
This commit is contained in:
parent
fe5df11b23
commit
2f02e56893
20
core/sandbox/README.md
Normal file
20
core/sandbox/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# JVM sandbox
|
||||
|
||||
The code in this submodule is not presently integrated with the rest of the platform and stands alone. It will
|
||||
eventually become a part of the node software and enforce deterministic and secure execution on smart contract
|
||||
code, which is mobile and may propagate around the network without human intervention. Note that this sandbox
|
||||
is not designed as a anti-DoS mitigation.
|
||||
|
||||
To learn more about the sandbox design please consult the Corda technical white paper.
|
||||
|
||||
This code was written by Ben Evans.
|
||||
|
||||
# Roadmap
|
||||
|
||||
* Thorough code and security review.
|
||||
* Possibly, a conversion to Kotlin.
|
||||
* Make the instrumentation ahead of time only.
|
||||
* Finalise the chosen subset of the Java platform to expose to contract code.
|
||||
* Create the pre-instrumented sandboxed class files and check them in.
|
||||
* Integrate with the AttachmentsClassLoader
|
||||
* Add OpenJDK/Avian patches for deterministic Object.hashCode() implementation.
|
31
core/sandbox/build.gradle
Normal file
31
core/sandbox/build.gradle
Normal file
@ -0,0 +1,31 @@
|
||||
group 'net.corda'
|
||||
version '0.6-SNAPSHOT'
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
buildscript {
|
||||
ext.asm_version = '5.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Asm: bytecode manipulation library.
|
||||
compile "org.ow2.asm:asm:$asm_version"
|
||||
compile "org.ow2.asm:asm-tree:$asm_version"
|
||||
compile "org.ow2.asm:asm-util:$asm_version"
|
||||
compile "org.ow2.asm:asm-commons:$asm_version"
|
||||
|
||||
// JOptSimple: command line option parsing
|
||||
compile "net.sf.jopt-simple:jopt-simple:5.0.1"
|
||||
|
||||
// Simple Logging Facade: makes the code independent of the chosen logging framework.
|
||||
compile "org.slf4j:slf4j-api:1.7.21"
|
||||
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
}
|
221
core/sandbox/src/main/java/com/r3cev/CandidacyStatus.java
Normal file
221
core/sandbox/src/main/java/com/r3cev/CandidacyStatus.java
Normal file
@ -0,0 +1,221 @@
|
||||
package com.r3cev;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static com.r3cev.CandidateMethod.State.*;
|
||||
|
||||
/**
|
||||
* Represents the status of the candidacy of a particular set of candidate methods, i.e. Their progress from
|
||||
* being {@link CandidateMethod.State#UNDETERMINED} to {@link CandidateMethod.State#DETERMINISTIC}
|
||||
* or {@link CandidateMethod.State#DISALLOWED} states.
|
||||
* A method is identified by a string that is encoded as a standard JVM representation
|
||||
* as per the constant pool. E.g.: java/lang/Byte.compareTo:(Ljava/lang/Byte;)I
|
||||
*/
|
||||
public class CandidacyStatus {
|
||||
|
||||
private static final int MAX_CLASSLOADING_RECURSIVE_DEPTH = 500;
|
||||
|
||||
private static final String DETERMINISTIC_METHODS = "java8.scan.java.lang_and_util"; //"java8.scan.java.lang
|
||||
|
||||
private final SortedMap<String, CandidateMethod> candidateMethods = new TreeMap<>();
|
||||
|
||||
// Backlog of methodSignatures that may have come in from other loaded classes
|
||||
private final Set<String> backlog = new LinkedHashSet<>();
|
||||
|
||||
private WhitelistClassLoader contextLoader;
|
||||
|
||||
// Loadable is true by default as it's easier to prove falsehood than truth
|
||||
private boolean loadable = true;
|
||||
|
||||
// If at all possible, we want to be able to provide a precise reason why this
|
||||
// class is not loadable. As some methods are determined to be showstoppers only
|
||||
// at the ClassVisitor it makes sense to store it here (for final reporting)
|
||||
// as well as in the CandidateMethod
|
||||
private WhitelistClassloadingException reason;
|
||||
|
||||
private int recursiveDepth = 0;
|
||||
|
||||
private CandidacyStatus() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param signature
|
||||
* @return true if the input was absent from the underlying map
|
||||
*/
|
||||
boolean putIfAbsent(final String signature, final CandidateMethod candidate) {
|
||||
return null == candidateMethods.putIfAbsent(signature, candidate);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param methodSignature
|
||||
* @return true if the input was absent from the underlying map
|
||||
*/
|
||||
public boolean putIfAbsent(final String methodSignature) {
|
||||
return null == candidateMethods.putIfAbsent(methodSignature, CandidateMethod.of(methodSignature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method
|
||||
*
|
||||
* @param startingSet
|
||||
* @return a candidacy status based on the starting set
|
||||
*/
|
||||
public static CandidacyStatus of(final String startingSet) {
|
||||
final CandidacyStatus baseCandidacyStatus = new CandidacyStatus();
|
||||
try {
|
||||
for (String s : baseCandidacyStatus.readLinesFromFile(startingSet)) {
|
||||
baseCandidacyStatus.putIfAbsent(s, CandidateMethod.proven(s));
|
||||
}
|
||||
} catch (IOException | URISyntaxException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
return baseCandidacyStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method
|
||||
*
|
||||
* @return a candidacy status based on the starting set
|
||||
*/
|
||||
public static CandidacyStatus of() {
|
||||
return CandidacyStatus.of(DETERMINISTIC_METHODS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional methods that are known to be deterministic
|
||||
*
|
||||
* @param methodNames
|
||||
*/
|
||||
public void addKnownDeterministicMethods(final Set<String> methodNames) {
|
||||
for (String known : methodNames) {
|
||||
candidateMethods.putIfAbsent(known, CandidateMethod.proven(known));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method for candidate methods
|
||||
*
|
||||
* @param methodSignature
|
||||
* @return the candidate method corresponding to a method signature
|
||||
*/
|
||||
public CandidateMethod getCandidateMethod(final String methodSignature) {
|
||||
return candidateMethods.get(methodSignature);
|
||||
}
|
||||
|
||||
public Map<String, CandidateMethod> getCandidateMethods() {
|
||||
return candidateMethods;
|
||||
}
|
||||
|
||||
public void addToBacklog(final String discoveredMethod) {
|
||||
if (!backlog.contains(discoveredMethod)) {
|
||||
backlog.add(discoveredMethod);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> readLinesFromFile(final String fName) throws IOException, URISyntaxException {
|
||||
final Path p = Paths.get(getClass().getClassLoader().getResource(fName).toURI());
|
||||
return Files.readAllLines(p);
|
||||
}
|
||||
|
||||
public boolean isLoadable() {
|
||||
return loadable;
|
||||
}
|
||||
|
||||
public void setLoadable(final boolean loadable) {
|
||||
this.loadable = loadable;
|
||||
}
|
||||
|
||||
public WhitelistClassloadingException getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(final String because) {
|
||||
reason = new WhitelistClassloadingException(because);
|
||||
}
|
||||
|
||||
public WhitelistClassLoader getContextLoader() {
|
||||
return contextLoader;
|
||||
}
|
||||
|
||||
public void setContextLoader(final WhitelistClassLoader contextLoader) {
|
||||
this.contextLoader = contextLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the recursive depth of this classloading process, throwing a
|
||||
* ClassNotFoundException if it becomes too high
|
||||
*
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
public void incRecursiveCount() throws ClassNotFoundException {
|
||||
if (recursiveDepth >= MAX_CLASSLOADING_RECURSIVE_DEPTH - 1) {
|
||||
reason = new WhitelistClassloadingException("Recursive depth of classloading exceeded");
|
||||
throw new ClassNotFoundException("Class cannot be loaded due to deep recursion", reason);
|
||||
}
|
||||
recursiveDepth++;
|
||||
}
|
||||
|
||||
public void decRecursiveCount() {
|
||||
recursiveDepth--;
|
||||
}
|
||||
|
||||
public Set<String> getDisallowedMethods() {
|
||||
final Set<String> out = new HashSet<>();
|
||||
for (final String candidateName : candidateMethods.keySet()) {
|
||||
final CandidateMethod candidate = candidateMethods.get(candidateName);
|
||||
if (candidate.getCurrentState() == DISALLOWED) {
|
||||
out.add(candidateName);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CandidacyStatus{" + "candidateMethods=" + candidateMethods + ", backlog=" + backlog + ", contextLoader=" + contextLoader + ", loadable=" + loadable + ", reason=" + reason + ", recursiveDepth=" + recursiveDepth + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 5;
|
||||
hash = 53 * hash + Objects.hashCode(this.candidateMethods);
|
||||
hash = 53 * hash + Objects.hashCode(this.backlog);
|
||||
hash = 53 * hash + Objects.hashCode(this.contextLoader);
|
||||
hash = 53 * hash + (this.loadable ? 1 : 0);
|
||||
hash = 53 * hash + Objects.hashCode(this.reason);
|
||||
hash = 53 * hash + this.recursiveDepth;
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
final CandidacyStatus other = (CandidacyStatus) obj;
|
||||
if (!Objects.equals(this.candidateMethods, other.candidateMethods))
|
||||
return false;
|
||||
if (!Objects.equals(this.backlog, other.backlog))
|
||||
return false;
|
||||
if (!Objects.equals(this.contextLoader, other.contextLoader))
|
||||
return false;
|
||||
if (this.loadable != other.loadable)
|
||||
return false;
|
||||
if (!Objects.equals(this.reason, other.reason))
|
||||
return false;
|
||||
if (this.recursiveDepth != other.recursiveDepth)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
110
core/sandbox/src/main/java/com/r3cev/CandidateMethod.java
Normal file
110
core/sandbox/src/main/java/com/r3cev/CandidateMethod.java
Normal file
@ -0,0 +1,110 @@
|
||||
package com.r3cev;
|
||||
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A candidate method that is under evaluation. Candidate methods have one of the following states:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link CandidateMethod.State#DETERMINISTIC} - It's deterministic and therefore is allowed to be loaded.</li>
|
||||
* <li>{@link CandidateMethod.State#DISALLOWED} - It's not deterministic and won't be allowed to be loaded.</li>
|
||||
* <li>{@link CandidateMethod.State#SCANNED} - We're not sure if it's deterministic or not.</li>
|
||||
* </ul>
|
||||
*
|
||||
* CandidateMethods themselves reference other CandidateMethods which are be checked for their deterministic state
|
||||
*
|
||||
*/
|
||||
public final class CandidateMethod {
|
||||
|
||||
// The state model must reflect the difference between "mentioned in an API" and
|
||||
// "scanned by classloader"
|
||||
public enum State {
|
||||
DETERMINISTIC,
|
||||
MENTIONED,
|
||||
SCANNED,
|
||||
DISALLOWED
|
||||
}
|
||||
|
||||
private State currentState = State.MENTIONED;
|
||||
|
||||
private String reason;
|
||||
|
||||
private CandidateMethod(String methodSignature) {
|
||||
internalMethodName = methodSignature;
|
||||
}
|
||||
|
||||
// TODO We'll likely use the formal MethodType for deeper analysis
|
||||
private MethodType methodType;
|
||||
|
||||
// Internal method name as it appears in the constant pool
|
||||
private final String internalMethodName;
|
||||
|
||||
private final Set<CandidateMethod> referencedCandidateMethods = new HashSet<>();
|
||||
|
||||
|
||||
public State getCurrentState() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
public void setCurrentState(final State currentState) {
|
||||
this.currentState = currentState;
|
||||
}
|
||||
|
||||
public void disallowed(final String because) {
|
||||
reason = because;
|
||||
currentState = State.DISALLOWED;
|
||||
}
|
||||
|
||||
public void deterministic() {
|
||||
if (currentState == State.DISALLOWED) {
|
||||
throw new IllegalArgumentException("Method "+ internalMethodName +" attempted to transition from DISALLOWED to DETERMINISTIC");
|
||||
}
|
||||
currentState = State.DETERMINISTIC;
|
||||
}
|
||||
|
||||
public void scanned() {
|
||||
currentState = State.SCANNED;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public String getInternalMethodName() {
|
||||
return internalMethodName;
|
||||
}
|
||||
|
||||
public void addReferencedCandidateMethod(final CandidateMethod referenceCandidateMethod) {
|
||||
referencedCandidateMethods.add(referenceCandidateMethod);
|
||||
}
|
||||
|
||||
public Set<CandidateMethod> getReferencedCandidateMethods() {
|
||||
return referencedCandidateMethods;
|
||||
}
|
||||
|
||||
public static CandidateMethod of(String methodSignature) {
|
||||
return new CandidateMethod(methodSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* This factory constructor is only called for methods that are known to be deterministic in advance
|
||||
* @param methodSignature
|
||||
* @return
|
||||
*/
|
||||
public static CandidateMethod proven(String methodSignature) {
|
||||
final CandidateMethod provenCandidateMethod = new CandidateMethod(methodSignature);
|
||||
provenCandidateMethod.deterministic();
|
||||
return provenCandidateMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CandidateMethod{" + "currentState=" + currentState + ", reason=" + reason + ", methodType=" + methodType + ", internalMethodName=" + internalMethodName + ", referencedCandidateMethods=" + referencedCandidateMethods + '}';
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package com.r3cev;
|
||||
|
||||
import static com.r3cev.Utils.*;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public final class SandboxAwareClassWriter extends ClassWriter {
|
||||
|
||||
private final ClassLoader loader;
|
||||
|
||||
public SandboxAwareClassWriter(final ClassLoader save, final ClassReader classReader, final int flags) {
|
||||
super(classReader, flags);
|
||||
loader = save;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common super type of the two given types. The default
|
||||
* implementation of this method <i>loads</i> the two given classes and uses
|
||||
* the java.lang.Class methods to find the common super class. It can be
|
||||
* overridden to compute this common super type in other ways, in particular
|
||||
* without actually loading any class, or to take into account the class
|
||||
* that is currently being generated by this ClassWriter, which can of
|
||||
* course not be loaded since it is under construction.
|
||||
*
|
||||
* @param type1
|
||||
* the internal name of a class.
|
||||
* @param type2
|
||||
* the internal name of another class.
|
||||
* @return the internal name of the common super class of the two given
|
||||
* classes.
|
||||
*/
|
||||
@Override
|
||||
public String getCommonSuperClass(final String type1, final String type2) {
|
||||
if (OBJECT.equals(type1) || OBJECT.equals(type2)
|
||||
|| OBJECT.equals(unsandboxNameIfNeedBe(type1)) || OBJECT.equals(unsandboxNameIfNeedBe(type2))) {
|
||||
return OBJECT;
|
||||
}
|
||||
// System.out.println(type1 + " ; " + type2);
|
||||
String out = super.getCommonSuperClass(unsandboxNameIfNeedBe(type1), unsandboxNameIfNeedBe(type2));
|
||||
// try {
|
||||
// out = getCommonSuperClassBorrowed(type1, type2);
|
||||
// } catch (final ClassNotFoundException cnfe) {
|
||||
// throw new RuntimeException(cnfe);
|
||||
// }
|
||||
if (SANDBOX_PATTERN_INTERNAL.asPredicate().test(type1) || SANDBOX_PATTERN_INTERNAL.asPredicate().test(type2)) {
|
||||
return SANDBOX_PREFIX_INTERNAL + out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public String getCommonSuperClassBorrowed(final String type1, final String type2) throws ClassNotFoundException {
|
||||
Class<?> c, d;
|
||||
try {
|
||||
c = Class.forName(type1.replace('/', '.'), false, loader);
|
||||
d = Class.forName(type2.replace('/', '.'), false, loader);
|
||||
} catch (Exception e) {
|
||||
|
||||
c = Class.forName(unsandboxNameIfNeedBe(type1).replace('/', '.'), false, loader);
|
||||
d = Class.forName(unsandboxNameIfNeedBe(type2).replace('/', '.'), false, loader);
|
||||
|
||||
// throw new RuntimeException(e.toString());
|
||||
}
|
||||
if (c.isAssignableFrom(d)) {
|
||||
return type1;
|
||||
}
|
||||
if (d.isAssignableFrom(c)) {
|
||||
return type2;
|
||||
}
|
||||
if (c.isInterface() || d.isInterface()) {
|
||||
return "java/lang/Object";
|
||||
} else {
|
||||
do {
|
||||
c = c.getSuperclass();
|
||||
} while (!c.isAssignableFrom(d));
|
||||
return c.getName().replace('.', '/');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
20
core/sandbox/src/main/java/com/r3cev/SandboxRemapper.java
Normal file
20
core/sandbox/src/main/java/com/r3cev/SandboxRemapper.java
Normal file
@ -0,0 +1,20 @@
|
||||
package com.r3cev;
|
||||
|
||||
import org.objectweb.asm.commons.Remapper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public final class SandboxRemapper extends Remapper {
|
||||
|
||||
@Override
|
||||
public String mapDesc(final String desc) {
|
||||
return super.mapDesc(Utils.rewriteDescInternal(desc));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String map(final String typename) {
|
||||
return super.map(Utils.sandboxInternalTypeName(typename));
|
||||
}
|
||||
}
|
211
core/sandbox/src/main/java/com/r3cev/Utils.java
Normal file
211
core/sandbox/src/main/java/com/r3cev/Utils.java
Normal file
@ -0,0 +1,211 @@
|
||||
package com.r3cev;
|
||||
|
||||
import com.r3cev.visitors.CostInstrumentingMethodVisitor;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.objectweb.asm.*;
|
||||
import org.objectweb.asm.commons.ClassRemapper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public final class Utils {
|
||||
|
||||
public final static String SANDBOX_PREFIX_INTERNAL = "sandbox/";
|
||||
|
||||
public final static String CLASSFILE_NAME_SUFFIX = "^(.*)\\.class$";
|
||||
|
||||
public static final Pattern JAVA_LANG_PATTERN_INTERNAL = Pattern.compile("^java/lang/(.*)");
|
||||
|
||||
public static final Pattern SANDBOX_PATTERN_INTERNAL = Pattern.compile("^" + SANDBOX_PREFIX_INTERNAL + "(.*)");
|
||||
|
||||
public static final Pattern SIGNATURE_PATTERN_INTERNAL = Pattern.compile("\\((.*)\\)(.+)");
|
||||
|
||||
public static final Pattern REFTYPE_PATTERN_INTERNAL = Pattern.compile("(L[^;]+;)");
|
||||
|
||||
public static final Pattern ARRAY_REFTYPE_PATTERN_INTERNAL = Pattern.compile("((\\[+)L[^;]+;)");
|
||||
|
||||
public static final Pattern JAVA_PATTERN_QUALIFIED = Pattern.compile("^java\\.(.+)");
|
||||
|
||||
public static final Pattern CLASSNAME_PATTERN_QUALIFIED = Pattern.compile("([^\\.]+)\\.");
|
||||
|
||||
public static final String OBJECT = "java/lang/Object";
|
||||
|
||||
public static final String THROWABLE = "java/lang/Throwable";
|
||||
|
||||
public static final String ERROR = "java/lang/Error";
|
||||
|
||||
public static final String THREAD_DEATH = "java/lang/ThreadDeath";
|
||||
|
||||
// Hide constructor
|
||||
private Utils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that converts from the internal class name format (as used in the
|
||||
* Constant Pool) to a fully-qualified class name. No obvious library method to do this
|
||||
* appears to exist, hence this code. If one exists, rip this out.
|
||||
* @param classInternalName
|
||||
* @return
|
||||
*/
|
||||
public static String convertInternalFormToQualifiedClassName(final String classInternalName) {
|
||||
String out = classInternalName.replaceAll("/", "\\.");
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes in an internal method name but needs to return a qualified
|
||||
* classname (suitable for loading)
|
||||
*
|
||||
*
|
||||
* @param internalMethodName
|
||||
* @return
|
||||
*/
|
||||
public static String convertInternalMethodNameToQualifiedClassName(final String internalMethodName) {
|
||||
final Matcher classMatch = CLASSNAME_PATTERN_QUALIFIED.matcher(internalMethodName);
|
||||
if (classMatch.find()) {
|
||||
return convertInternalFormToQualifiedClassName(classMatch.group(1));
|
||||
} else {
|
||||
throw new IllegalArgumentException(internalMethodName + " is not a legal method name");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that converts from a fully-qualified class name to the internal class
|
||||
* name format (as used in the Constant Pool). No obvious library method to do this
|
||||
* appears to exist, hence this code. If one exists, rip this out.
|
||||
* @param qualifiedClassName
|
||||
* @return
|
||||
*/
|
||||
public static String convertQualifiedClassNameToInternalForm(final String qualifiedClassName) {
|
||||
String out = qualifiedClassName.replaceAll("\\.", "/");
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method potentially rewrites the classname.
|
||||
*
|
||||
* @param internalClassname - specified in internal form
|
||||
* @return
|
||||
*/
|
||||
public static String sandboxInternalTypeName(final String internalClassname) {
|
||||
if (classShouldBeSandboxedInternal(internalClassname)) {
|
||||
final Matcher arrayMatch = ARRAY_REFTYPE_PATTERN_INTERNAL.matcher(internalClassname);
|
||||
if (arrayMatch.find()) {
|
||||
final String indirection = arrayMatch.group(2);
|
||||
return indirection + SANDBOX_PREFIX_INTERNAL + internalClassname.substring(indirection.length());
|
||||
} else {
|
||||
// Regular, non-array reftype
|
||||
return SANDBOX_PREFIX_INTERNAL + internalClassname;
|
||||
}
|
||||
}
|
||||
|
||||
return internalClassname;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param qualifiedTypeName
|
||||
* @return
|
||||
*/
|
||||
public static String sandboxQualifiedTypeName(final String qualifiedTypeName) {
|
||||
final String internal = convertQualifiedClassNameToInternalForm(qualifiedTypeName);
|
||||
final String sandboxedInternal = sandboxInternalTypeName(internal);
|
||||
if (internal.equals(sandboxedInternal)) {
|
||||
return qualifiedTypeName;
|
||||
}
|
||||
return convertInternalFormToQualifiedClassName(sandboxedInternal);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method removes the sandboxing prefix from a method or type name, if it has
|
||||
* one, otherwise it returns the input string.
|
||||
*
|
||||
* @param internalClassname
|
||||
* @return the internal classname, unsandboxed if that was required
|
||||
*/
|
||||
public static String unsandboxNameIfNeedBe(final String internalClassname) {
|
||||
final Matcher m = SANDBOX_PATTERN_INTERNAL.matcher(internalClassname);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
return internalClassname;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param desc - internal
|
||||
* @return the rewritten desc string
|
||||
*/
|
||||
public static String rewriteDescInternal(final String desc) {
|
||||
String remaining = desc;
|
||||
final Matcher formatCheck = SIGNATURE_PATTERN_INTERNAL.matcher(desc);
|
||||
// Check it's a valid signature string
|
||||
if (!formatCheck.find())
|
||||
return remaining;
|
||||
|
||||
final StringBuilder out = new StringBuilder();
|
||||
while (remaining.length() > 0) {
|
||||
final Matcher refTypeFound = REFTYPE_PATTERN_INTERNAL.matcher(remaining);
|
||||
if (refTypeFound.find()) {
|
||||
final int startOfType = refTypeFound.start();
|
||||
final int endOfType = refTypeFound.end();
|
||||
final String before = remaining.substring(0, startOfType);
|
||||
|
||||
final String found = refTypeFound.group(1);
|
||||
final String rewritten = "L" + sandboxInternalTypeName(found.substring(1));
|
||||
out.append(before);
|
||||
out.append(rewritten);
|
||||
remaining = remaining.substring(endOfType);
|
||||
} else {
|
||||
out.append(remaining);
|
||||
remaining = "";
|
||||
}
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a classname in qualified form is a candidate for transitive
|
||||
* loading. This should not attempt to load a classname that starts with java. as
|
||||
* the only permissable classes have already been transformed into sandboxed
|
||||
* methods
|
||||
*
|
||||
* @param qualifiedClassName
|
||||
* @return
|
||||
*/
|
||||
public static boolean shouldAttemptToTransitivelyLoad(final String qualifiedClassName) {
|
||||
if (JAVA_PATTERN_QUALIFIED.asPredicate().test(qualifiedClassName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that determines whether this class requires sandboxing
|
||||
*
|
||||
* @param clazzName - specified in internal form
|
||||
* @return true if the class should be sandboxed
|
||||
*/
|
||||
public static boolean classShouldBeSandboxedInternal(final String clazzName) {
|
||||
if (ARRAY_REFTYPE_PATTERN_INTERNAL.asPredicate().test(clazzName)) {
|
||||
return classShouldBeSandboxedInternal(clazzName.substring(2, clazzName.length() - 1));
|
||||
}
|
||||
|
||||
if (JAVA_LANG_PATTERN_INTERNAL.asPredicate().test(clazzName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SANDBOX_PATTERN_INTERNAL.asPredicate().test(clazzName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
357
core/sandbox/src/main/java/com/r3cev/WhitelistClassLoader.java
Normal file
357
core/sandbox/src/main/java/com/r3cev/WhitelistClassLoader.java
Normal file
@ -0,0 +1,357 @@
|
||||
package com.r3cev;
|
||||
|
||||
import com.r3cev.visitors.CostInstrumentingMethodVisitor;
|
||||
import com.r3cev.visitors.WhitelistCheckingClassVisitor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import org.objectweb.asm.*;
|
||||
import org.objectweb.asm.commons.ClassRemapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public final class WhitelistClassLoader extends ClassLoader {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WhitelistClassLoader.class);
|
||||
|
||||
private final Map<String, Class<?>> loadedClasses;
|
||||
|
||||
private final Map<String, byte[]> transformedClasses;
|
||||
|
||||
private final List<Path> primaryClasspathSearchPath = new ArrayList<>();
|
||||
|
||||
private final List<Path> fileSystemSearchPath = new ArrayList<>();
|
||||
|
||||
private final CandidacyStatus candidacyStatus;
|
||||
|
||||
private final boolean removeNonDeterministicMethods;
|
||||
|
||||
private Path classDir;
|
||||
|
||||
private String classInternalName;
|
||||
|
||||
private Path outputJarPath;
|
||||
|
||||
private WhitelistClassLoader(final boolean stripNonDeterministicMethods) {
|
||||
candidacyStatus = CandidacyStatus.of();
|
||||
loadedClasses = new HashMap<>();
|
||||
transformedClasses = new HashMap<>();
|
||||
removeNonDeterministicMethods = stripNonDeterministicMethods;
|
||||
}
|
||||
|
||||
/*
|
||||
* Copy constructor for use in recursive calls
|
||||
* @param other
|
||||
*/
|
||||
private WhitelistClassLoader(WhitelistClassLoader other) {
|
||||
candidacyStatus = other.candidacyStatus;
|
||||
loadedClasses = other.loadedClasses;
|
||||
transformedClasses = other.transformedClasses;
|
||||
fileSystemSearchPath.addAll(other.fileSystemSearchPath);
|
||||
primaryClasspathSearchPath.addAll(other.primaryClasspathSearchPath);
|
||||
removeNonDeterministicMethods = other.removeNonDeterministicMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method. Throws URISyntaxException currently, as this method is
|
||||
* called with user data, so a checked exception is not unreasonable. Could use a
|
||||
* runtime exception instead.
|
||||
*
|
||||
* @param auxiliaryClassPath
|
||||
* @param stripNonDeterministic if set to true, then rather than requiring all
|
||||
* methods to be deterministic, instead the classloader
|
||||
* will remove all non-deterministic methods.
|
||||
* @return a suitably constructed whitelisting classloader
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
public static WhitelistClassLoader of(final String auxiliaryClassPath, final boolean stripNonDeterministic) throws URISyntaxException {
|
||||
final WhitelistClassLoader out = new WhitelistClassLoader(stripNonDeterministic);
|
||||
out.candidacyStatus.setContextLoader(out);
|
||||
out.setupClasspath(auxiliaryClassPath);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static WhitelistClassLoader of(final String auxiliaryClassPath) throws URISyntaxException {
|
||||
return of(auxiliaryClassPath, false);
|
||||
}
|
||||
|
||||
public static WhitelistClassLoader of(final Path auxiliaryJar, final boolean stripNonDeterministic) throws URISyntaxException {
|
||||
final WhitelistClassLoader out = new WhitelistClassLoader(stripNonDeterministic);
|
||||
out.candidacyStatus.setContextLoader(out);
|
||||
out.fileSystemSearchPath.add(auxiliaryJar);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static WhitelistClassLoader of(final Path auxiliaryJar) throws URISyntaxException {
|
||||
return of(auxiliaryJar, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method. Used for recursive classloading
|
||||
*
|
||||
* @param other
|
||||
* @return a suitably constructed whitelisting classloader based on the state
|
||||
* of the passed classloader
|
||||
*/
|
||||
public static WhitelistClassLoader of(final WhitelistClassLoader other) {
|
||||
final WhitelistClassLoader out = new WhitelistClassLoader(other);
|
||||
// out.candidacyStatus.setContextLoader(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that adds a jar to the path to be searched
|
||||
*
|
||||
* @param knownGoodJar
|
||||
*/
|
||||
void addJarToSandbox(final Path knownGoodJar) {
|
||||
fileSystemSearchPath.add(knownGoodJar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the auxiliary classpath so that classes that are not on the original
|
||||
* classpath can be scanned for.
|
||||
* Note that this this method hardcodes Unix conventions, so won't work on e.g. Windows
|
||||
*
|
||||
* @param auxiliaryClassPath
|
||||
* @throws URISyntaxException
|
||||
*/
|
||||
void setupClasspath(final String auxiliaryClassPath) throws URISyntaxException {
|
||||
for (String entry : auxiliaryClassPath.split(":")) {
|
||||
if (entry.startsWith("/")) {
|
||||
fileSystemSearchPath.add(Paths.get(entry));
|
||||
} else {
|
||||
final URL u = getClass().getClassLoader().getResource(entry);
|
||||
primaryClasspathSearchPath.add(Paths.get(u.toURI()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param qualifiedClassName
|
||||
* @return a class object that has been whitelist checked and is known to be
|
||||
* deterministic
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
@Override
|
||||
public Class<?> findClass(final String qualifiedClassName) throws ClassNotFoundException {
|
||||
// One problem is that the requested class may refer to untransformed (but
|
||||
// deterministic) classes that will resolve & be loadable by the WLCL, but
|
||||
// in doing so, the name of the referenced class is rewritten and the name
|
||||
// by which it is now known does not have a mapping to a loaded class.
|
||||
// To solve this, we use the loadedClasses cache - on both possible keys
|
||||
// for the class (either of which will point to a transformed class object)
|
||||
Class<?> cls = loadedClasses.get(qualifiedClassName);
|
||||
if (cls != null) {
|
||||
return cls;
|
||||
}
|
||||
final String sandboxed = Utils.sandboxQualifiedTypeName(qualifiedClassName);
|
||||
cls = loadedClasses.get(sandboxed);
|
||||
if (cls != null) {
|
||||
return cls;
|
||||
}
|
||||
// Cache miss - so now try the superclass implementation
|
||||
try {
|
||||
cls = super.findClass(qualifiedClassName);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// We actually need to load this ourselves, so find the path
|
||||
// corresponding to the directory where the classfile lives.
|
||||
// Note that for jar files this might be a "virtual" Path object
|
||||
classInternalName = Utils.convertQualifiedClassNameToInternalForm(qualifiedClassName);
|
||||
classDir = locateClassfileDir(classInternalName);
|
||||
try {
|
||||
final boolean isDeterministic = scan();
|
||||
if (isDeterministic || removeNonDeterministicMethods) {
|
||||
final Path fullPathToClass = classDir.resolve(classInternalName + ".class");
|
||||
Set<String> methodsToRemove = new HashSet<>();
|
||||
if (removeNonDeterministicMethods && !isDeterministic) {
|
||||
methodsToRemove = candidacyStatus.getDisallowedMethods();
|
||||
}
|
||||
|
||||
final byte[] classContents = Files.readAllBytes(fullPathToClass);
|
||||
final byte[] instrumentedBytes = instrumentWithCosts(classContents, methodsToRemove);
|
||||
if (!removeNonDeterministicMethods) {
|
||||
// If we're in stripping mode, then trying to define the class
|
||||
// will cause a transitive loading failure
|
||||
cls = defineClass(null, instrumentedBytes, 0, instrumentedBytes.length);
|
||||
}
|
||||
transformedClasses.put(sandboxed, instrumentedBytes);
|
||||
} else {
|
||||
throw new ClassNotFoundException("Class " + qualifiedClassName + " could not be loaded.", reason());
|
||||
}
|
||||
} catch (final IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Saving class " + cls + " as " + qualifiedClassName);
|
||||
|
||||
loadedClasses.put(qualifiedClassName, cls);
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Saving class " + cls + " as " + sandboxed);
|
||||
|
||||
loadedClasses.put(sandboxed, cls);
|
||||
|
||||
return cls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the ASM library read in the currentClass's byte code and visit the call
|
||||
* sites within it. Whilst visiting, check to see if the classes/methods visited
|
||||
* are deterministic and therefore safe to load.
|
||||
*
|
||||
* @return true if the current class is safe to be loaded
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public boolean scan() throws IOException {
|
||||
try (final InputStream in = Files.newInputStream(classDir.resolve(classInternalName + ".class"))) {
|
||||
try {
|
||||
final ClassReader classReader = new ClassReader(in);
|
||||
|
||||
// Useful for debug, you can pass in the traceClassVisitor as an extra parameter if needed
|
||||
// PrintWriter printWriter = new PrintWriter(System.out);
|
||||
// TraceClassVisitor traceClassVisitor = new TraceClassVisitor(printWriter);
|
||||
final ClassVisitor whitelistCheckingClassVisitor
|
||||
= new WhitelistCheckingClassVisitor(classInternalName, candidacyStatus);
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("About to read class: " + classInternalName);
|
||||
|
||||
// If there's debug info in the class, don't look at that whilst visiting
|
||||
classReader.accept(whitelistCheckingClassVisitor, ClassReader.SKIP_DEBUG);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.error("Exception whilst reading class: " + classInternalName, ex);
|
||||
}
|
||||
}
|
||||
|
||||
return candidacyStatus.isLoadable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that takes a class name (in internal format) and returns a Path
|
||||
* corresponding to the dir where the classfile was found. We are essentially working
|
||||
* around a limitation of the ASM library that does not integrate cleanly with Java 7
|
||||
* NIO.2 Path APIs. This method also performs a couple of basic sanity check on the
|
||||
* class file (e.g. that it exists, is a regular file and is readable).
|
||||
*
|
||||
* @param internalClassName
|
||||
* @return a path object that corresponds to a class that has been found
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
Path locateClassfileDir(final String internalClassName) throws ClassNotFoundException {
|
||||
// Check the primaryClasspathSearchPath
|
||||
for (final Path p : primaryClasspathSearchPath) {
|
||||
final Path check = Paths.get(p.toString(), internalClassName + ".class");
|
||||
|
||||
if (Files.isRegularFile(check)) {
|
||||
if (!Files.isReadable(check)) {
|
||||
throw new IllegalArgumentException("File " + check + " found but is not readable");
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
for (final Path p : fileSystemSearchPath) {
|
||||
final Path check = p.resolve(internalClassName + ".class");
|
||||
if (Files.isRegularFile(check)) {
|
||||
if (!Files.isReadable(check)) {
|
||||
throw new IllegalArgumentException("File " + check + " found but is not readable");
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ClassNotFoundException("Requested class "
|
||||
+ Utils.convertInternalFormToQualifiedClassName(internalClassName) + " could not be found");
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments a class with runtime cost accounting
|
||||
*
|
||||
* @param originalClassContents
|
||||
* @param methodsToRemove
|
||||
* @return the byte array that represents the transformed class
|
||||
*/
|
||||
public byte[] instrumentWithCosts(final byte[] originalClassContents, final Set<String> methodsToRemove) {
|
||||
final ClassReader reader = new ClassReader(originalClassContents);
|
||||
final ClassWriter writer = new SandboxAwareClassWriter(this, reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
final ClassVisitor remapper = new ClassRemapper(writer, new SandboxRemapper());
|
||||
final ClassVisitor coster = new ClassVisitor(Opcodes.ASM5, remapper) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
|
||||
final MethodVisitor baseMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
|
||||
return new CostInstrumentingMethodVisitor(baseMethodVisitor, access, name, desc);
|
||||
}
|
||||
};
|
||||
reader.accept(coster, ClassReader.EXPAND_FRAMES);
|
||||
return writer.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a jar archive of all the transformed classes that this classloader
|
||||
* has loaded.
|
||||
*
|
||||
* @return true on success, false on failure
|
||||
* @throws java.io.IOException
|
||||
* @throws java.net.URISyntaxException
|
||||
*/
|
||||
public boolean createJar() throws IOException, URISyntaxException {
|
||||
final Map<String, String> env = new HashMap<>();
|
||||
env.put("create", String.valueOf(!outputJarPath.toFile().exists()));
|
||||
|
||||
final URI fileUri = outputJarPath.toUri();
|
||||
final URI zipUri = new URI("jar:" + fileUri.getScheme(), fileUri.getPath(), null);
|
||||
|
||||
try (final FileSystem zfs = FileSystems.newFileSystem(zipUri, env)) {
|
||||
final Path jarRoot = zfs.getRootDirectories().iterator().next();
|
||||
|
||||
for (final String newName : transformedClasses.keySet()) {
|
||||
final byte[] newClassDef = transformedClasses.get(newName);
|
||||
final String relativePathName = Utils.convertQualifiedClassNameToInternalForm(newName) + ".class";
|
||||
final Path outPath = jarRoot.resolve(relativePathName);
|
||||
|
||||
Files.createDirectories(outPath.getParent());
|
||||
Files.write(outPath, newClassDef);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method for the reason for failure
|
||||
* @return
|
||||
*/
|
||||
public WhitelistClassloadingException reason() {
|
||||
return candidacyStatus.getReason();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method for the method candidacy status
|
||||
* @return
|
||||
*/
|
||||
public CandidacyStatus getCandidacyStatus() {
|
||||
return candidacyStatus;
|
||||
}
|
||||
|
||||
public Path getOutpurJarPath() {
|
||||
return outputJarPath;
|
||||
}
|
||||
|
||||
public void setOutpurJarPath(Path outpurJarPath) {
|
||||
this.outputJarPath = outpurJarPath;
|
||||
}
|
||||
|
||||
public Set<String> cachedClasses() {
|
||||
return loadedClasses.keySet();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.r3cev;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class WhitelistClassloadingException extends Exception {
|
||||
|
||||
public WhitelistClassloadingException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public WhitelistClassloadingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WhitelistClassloadingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public WhitelistClassloadingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
protected WhitelistClassloadingException(String message, Throwable cause,
|
||||
boolean enableSuppression,
|
||||
boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
|
||||
}
|
48
core/sandbox/src/main/java/com/r3cev/costing/Contract.java
Normal file
48
core/sandbox/src/main/java/com/r3cev/costing/Contract.java
Normal file
@ -0,0 +1,48 @@
|
||||
package com.r3cev.costing;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class is the runtime representation of a running contract.
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public class Contract {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Contract.class);
|
||||
|
||||
private final RuntimeCostAccounter accountant = new RuntimeCostAccounter();
|
||||
private final Thread contractThread;
|
||||
private final Class<?> vettedCode;
|
||||
private final ContractExecutor executionStrategy;
|
||||
|
||||
public Contract(final Class<?> newCode, final ContractExecutor strategy) {
|
||||
vettedCode = newCode;
|
||||
executionStrategy = strategy;
|
||||
contractThread = new Thread(() -> executionStrategy.execute(this));
|
||||
contractThread.setName("ContractThread-" + System.currentTimeMillis());
|
||||
contractThread.setDaemon(true);
|
||||
}
|
||||
|
||||
public boolean isViable() {
|
||||
return executionStrategy.isSuitable(this);
|
||||
}
|
||||
|
||||
public Thread getThread() {
|
||||
return contractThread;
|
||||
}
|
||||
|
||||
public Class<?> getCode() {
|
||||
return vettedCode;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
contractThread.start();
|
||||
}
|
||||
|
||||
void suicide() {
|
||||
LOGGER.info("Terminating contract " + this);
|
||||
throw new ThreadDeath();
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.r3cev.costing;
|
||||
|
||||
/**
|
||||
* This interface is to decouple the actual executable code from the entry point and
|
||||
* how vetted deterministic code will be used inside the sandbox
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public interface ContractExecutor {
|
||||
/**
|
||||
* Executes a smart contract
|
||||
*
|
||||
* @param contract the contract to be executed
|
||||
*/
|
||||
void execute(Contract contract);
|
||||
|
||||
/**
|
||||
* Checks to see if the supplied Contract is suitable
|
||||
*
|
||||
* @param contract
|
||||
* @return true if the contract is suitable for execution
|
||||
*/
|
||||
boolean isSuitable(Contract contract);
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
package com.r3cev.costing;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public class RuntimeCostAccounter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeCostAccounter.class);
|
||||
|
||||
private static Thread primaryThread;
|
||||
|
||||
private static final ThreadLocal<Long> allocationCost = new ThreadLocal<Long>() {
|
||||
@Override
|
||||
protected Long initialValue() {
|
||||
return 0L;
|
||||
}
|
||||
};
|
||||
|
||||
private static final ThreadLocal<Long> jumpCost = new ThreadLocal<Long>() {
|
||||
@Override
|
||||
protected Long initialValue() {
|
||||
return 0L;
|
||||
}
|
||||
};
|
||||
|
||||
private static final ThreadLocal<Long> invokeCost = new ThreadLocal<Long>() {
|
||||
@Override
|
||||
protected Long initialValue() {
|
||||
return 0L;
|
||||
}
|
||||
};
|
||||
|
||||
private static final ThreadLocal<Long> throwCost = new ThreadLocal<Long>() {
|
||||
@Override
|
||||
protected Long initialValue() {
|
||||
return 0L;
|
||||
}
|
||||
};
|
||||
|
||||
private static final long BASELINE_ALLOC_KILL_THRESHOLD = 1024 * 1024;
|
||||
|
||||
private static final long BASELINE_JUMP_KILL_THRESHOLD = 100;
|
||||
|
||||
private static final long BASELINE_INVOKE_KILL_THRESHOLD = 100;
|
||||
|
||||
private static final long BASELINE_THROW_KILL_THRESHOLD = 50;
|
||||
|
||||
public static void recordJump() {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (current == primaryThread)
|
||||
return;
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("In recordJump() at " + System.currentTimeMillis() + " on " + current.getName());
|
||||
checkJumpCost(1);
|
||||
}
|
||||
|
||||
public static void recordAllocation(final String typeName) {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (current == primaryThread)
|
||||
return;
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("In recordAllocation() at " + System.currentTimeMillis()
|
||||
+ ", got object type: " + typeName + " on " + current.getName());
|
||||
|
||||
// More sophistication is clearly possible, e.g. caching approximate sizes for types that we encounter
|
||||
checkAllocationCost(1);
|
||||
}
|
||||
|
||||
public static void recordArrayAllocation(final int length, final int multiplier) {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (current == primaryThread)
|
||||
return;
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("In recordArrayAllocation() at " + System.currentTimeMillis()
|
||||
+ ", got array element size: " + multiplier + " and size: " + length + " on " + current.getName());
|
||||
|
||||
checkAllocationCost(length * multiplier);
|
||||
}
|
||||
|
||||
public static void recordMethodCall() {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (current == primaryThread)
|
||||
return;
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("In recordMethodCall() at " + System.currentTimeMillis() + " on " + current.getName());
|
||||
|
||||
checkInvokeCost(1);
|
||||
}
|
||||
|
||||
public static void recordThrow() {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (current == primaryThread)
|
||||
return;
|
||||
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("In recordThrow() at " + System.currentTimeMillis() + " on " + current.getName());
|
||||
checkThrowCost(1);
|
||||
}
|
||||
|
||||
public static void setPrimaryThread(final Thread toBeIgnored) {
|
||||
primaryThread = toBeIgnored;
|
||||
}
|
||||
|
||||
private static void checkAllocationCost(final long additional) {
|
||||
final long newValue = additional + allocationCost.get();
|
||||
allocationCost.set(newValue);
|
||||
if (newValue > BASELINE_ALLOC_KILL_THRESHOLD) {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Contract " + current + " terminated for overallocation");
|
||||
throw new ThreadDeath();
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkJumpCost(final long additional) {
|
||||
final long newValue = additional + jumpCost.get();
|
||||
jumpCost.set(newValue);
|
||||
if (newValue > BASELINE_JUMP_KILL_THRESHOLD) {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Contract " + current + " terminated for excessive use of looping");
|
||||
throw new ThreadDeath();
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkInvokeCost(final long additional) {
|
||||
final long newValue = additional + invokeCost.get();
|
||||
invokeCost.set(newValue);
|
||||
if (newValue > BASELINE_INVOKE_KILL_THRESHOLD) {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Contract " + current + " terminated for excessive method calling");
|
||||
throw new ThreadDeath();
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkThrowCost(final long additional) {
|
||||
final long newValue = additional + throwCost.get();
|
||||
throwCost.set(newValue);
|
||||
if (newValue > BASELINE_THROW_KILL_THRESHOLD) {
|
||||
final Thread current = Thread.currentThread();
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Contract " + current + " terminated for excessive exception throwing");
|
||||
throw new ThreadDeath();
|
||||
}
|
||||
}
|
||||
|
||||
public static long getAllocationCost() {
|
||||
return allocationCost.get();
|
||||
}
|
||||
|
||||
public static long getJumpCost() {
|
||||
return jumpCost.get();
|
||||
}
|
||||
|
||||
public static long getInvokeCost() {
|
||||
return invokeCost.get();
|
||||
}
|
||||
|
||||
public static long getThrowCost() {
|
||||
return throwCost.get();
|
||||
}
|
||||
|
||||
public static void resetCounters() {
|
||||
allocationCost.set(0L);
|
||||
jumpCost.set(0L);
|
||||
invokeCost.set(0L);
|
||||
throwCost.set(0L);
|
||||
}
|
||||
}
|
127
core/sandbox/src/main/java/com/r3cev/tools/SandboxCreator.java
Normal file
127
core/sandbox/src/main/java/com/r3cev/tools/SandboxCreator.java
Normal file
@ -0,0 +1,127 @@
|
||||
package com.r3cev.tools;
|
||||
|
||||
import com.r3cev.WhitelistClassLoader;
|
||||
import com.r3cev.visitors.SandboxPathVisitor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import joptsimple.OptionParser;
|
||||
import joptsimple.OptionSet;
|
||||
|
||||
/**
|
||||
* This class takes in an exploded set of JRE classes, and a whitelist, and rewrites all
|
||||
* classes (note: not methods) that have at least one whitelisted method to create a
|
||||
* sandboxed version of the class.
|
||||
*
|
||||
*/
|
||||
// java8.scan.java.lang_and_util java8.interfaces_for_compat java8 sandbox
|
||||
public final class SandboxCreator {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SandboxCreator.class);
|
||||
private static final String USAGE_STRING = "Usage: SandboxCreator <classes root dir> <output sandbox jar>";
|
||||
|
||||
private final String basePathName;
|
||||
private final String outputJarName;
|
||||
private final WhitelistClassLoader wlcl;
|
||||
private final boolean hasInputJar;
|
||||
|
||||
private final static OptionParser parser = new OptionParser();
|
||||
|
||||
private static void usage() {
|
||||
System.err.println(USAGE_STRING);
|
||||
}
|
||||
|
||||
private SandboxCreator(final OptionSet options) throws URISyntaxException {
|
||||
basePathName = (String) (options.valueOf("dir"));
|
||||
outputJarName = (String) (options.valueOf("out"));
|
||||
wlcl = WhitelistClassLoader.of(basePathName, true);
|
||||
hasInputJar = false;
|
||||
}
|
||||
|
||||
private SandboxCreator(final String tmpDirName, final OptionSet options) throws URISyntaxException {
|
||||
basePathName = tmpDirName;
|
||||
outputJarName = (String) (options.valueOf("out"));
|
||||
wlcl = WhitelistClassLoader.of(basePathName, true);
|
||||
hasInputJar = true;
|
||||
}
|
||||
|
||||
static String unpackJar(final String zipFilePath) throws IOException {
|
||||
final Path tmpDir = Files.createTempDirectory(Paths.get("/tmp"), "wlcl-extract");
|
||||
|
||||
try (final ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
|
||||
while (entry != null) {
|
||||
final Path newFile = tmpDir.resolve(entry.getName());
|
||||
if (!entry.isDirectory()) {
|
||||
Files.copy(zipIn, newFile);
|
||||
} else {
|
||||
Files.createDirectory(newFile);
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
entry = zipIn.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
return tmpDir.toString();
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
if (hasInputJar) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static SandboxCreator of(final OptionSet options) throws URISyntaxException, IOException {
|
||||
final String inputJarName = (String) (options.valueOf("jar"));
|
||||
if (inputJarName != null) {
|
||||
final String tmpDirName = unpackJar(inputJarName);
|
||||
return new SandboxCreator(tmpDirName, options);
|
||||
}
|
||||
return new SandboxCreator(options);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException, URISyntaxException {
|
||||
parser.accepts("help", "Displays this help screen").forHelp();
|
||||
parser.accepts("dir", "The directory where classes to be sandboxed can be found").withRequiredArg().ofType(String.class);
|
||||
parser.accepts("jar", "The jar file where classes to be sandboxed can be found").withRequiredArg().ofType(String.class);
|
||||
parser.accepts("out", "The output jar file where rewritten classes will be found").withRequiredArg().ofType(String.class);
|
||||
|
||||
final OptionSet options = parser.parse(args);
|
||||
|
||||
if (options.has("help")) {
|
||||
parser.printHelpOn(System.out);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
final SandboxCreator sandboxer = SandboxCreator.of(options);
|
||||
sandboxer.walk();
|
||||
sandboxer.writeJar();
|
||||
sandboxer.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param basePath
|
||||
* @param packageName
|
||||
* @throws IOException
|
||||
*/
|
||||
void walk() throws IOException {
|
||||
final Path scanDir = Paths.get(basePathName);
|
||||
final SandboxPathVisitor visitor = new SandboxPathVisitor(wlcl, scanDir);
|
||||
Files.walkFileTree(scanDir, visitor);
|
||||
}
|
||||
|
||||
private void writeJar() throws IOException, URISyntaxException {
|
||||
// When this method is called, wlcl should have loaded absolutely everything...
|
||||
Path outJar = Paths.get(outputJarName);
|
||||
wlcl.setOutpurJarPath(outJar);
|
||||
wlcl.createJar();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
package com.r3cev.visitors;
|
||||
|
||||
import com.r3cev.Utils;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.commons.GeneratorAdapter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public final class CostInstrumentingMethodVisitor extends GeneratorAdapter {
|
||||
|
||||
public static final int OP_BREAKPOINT = 0b1100_1010;
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CostInstrumentingMethodVisitor.class);
|
||||
|
||||
// In future, we may want to have multiple different accounting types
|
||||
// for e.g. benchmarking and to determine this at classloading time
|
||||
// might be helpful. We may want additional flexibility, e.g. to determine
|
||||
// the different accounting instrumenations separately, but this is a good
|
||||
// stub
|
||||
private final String runtimeAccounterTypeName;
|
||||
|
||||
public CostInstrumentingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String desc) {
|
||||
super(Opcodes.ASM5, methodVisitor, access, name, desc);
|
||||
|
||||
runtimeAccounterTypeName = "com/r3cev/costing/RuntimeCostAccounter";
|
||||
// save other calling parameters as well...?
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method replaces MONITORENTER / MONITOREXIT opcodes with POP - basically
|
||||
* stripping the synchronization out of any sandboxed code.
|
||||
* @param opcode
|
||||
*/
|
||||
@Override
|
||||
public void visitInsn(final int opcode) {
|
||||
switch (opcode) {
|
||||
case Opcodes.MONITORENTER:
|
||||
case Opcodes.MONITOREXIT:
|
||||
super.visitInsn(Opcodes.POP);
|
||||
return;
|
||||
case Opcodes.ATHROW:
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, runtimeAccounterTypeName, "recordThrow", "()V", false);
|
||||
break;
|
||||
case OP_BREAKPOINT:
|
||||
throw new IllegalStateException("Illegal opcode BREAKPOINT seen");
|
||||
}
|
||||
|
||||
super.visitInsn(opcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when visiting an opcode with a single int operand.
|
||||
* For our purposes this is a NEWARRAY opcode.
|
||||
*
|
||||
* @param opcode
|
||||
* @param operand
|
||||
*/
|
||||
@Override
|
||||
public void visitIntInsn(final int opcode, final int operand) {
|
||||
if (opcode != Opcodes.NEWARRAY) {
|
||||
super.visitIntInsn(opcode, operand);
|
||||
return;
|
||||
}
|
||||
|
||||
// Opcode is NEWARRAY - recordArrayAllocation:(Ljava/lang/String;I)V
|
||||
// operand value should be one of Opcodes.T_BOOLEAN,
|
||||
// Opcodes.T_CHAR, Opcodes.T_FLOAT, Opcodes.T_DOUBLE, Opcodes.T_BYTE,
|
||||
// Opcodes.T_SHORT, Opcodes.T_INT or Opcodes.T_LONG.
|
||||
final int typeSize;
|
||||
switch (operand) {
|
||||
case Opcodes.T_BOOLEAN:
|
||||
case Opcodes.T_BYTE:
|
||||
typeSize = 1;
|
||||
break;
|
||||
case Opcodes.T_SHORT:
|
||||
case Opcodes.T_CHAR:
|
||||
typeSize = 2;
|
||||
break;
|
||||
case Opcodes.T_INT:
|
||||
case Opcodes.T_FLOAT:
|
||||
typeSize = 4;
|
||||
break;
|
||||
case Opcodes.T_LONG:
|
||||
case Opcodes.T_DOUBLE:
|
||||
typeSize = 8;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Illegal operand to NEWARRAY seen: " + operand);
|
||||
}
|
||||
super.visitInsn(Opcodes.DUP);
|
||||
super.visitLdcInsn(typeSize);
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, runtimeAccounterTypeName, "recordArrayAllocation", "(II)V", true);
|
||||
super.visitIntInsn(opcode, operand);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when visiting an opcode with a single operand, that
|
||||
* is a type (represented here as a String).
|
||||
*
|
||||
* For our purposes this is either a NEW opcode or a ANEWARRAY
|
||||
*
|
||||
* @param opcode
|
||||
* @param type
|
||||
*/
|
||||
@Override
|
||||
public void visitTypeInsn(final int opcode, final String type) {
|
||||
// opcode is either NEW - recordAllocation:(Ljava/lang/String;)V
|
||||
// or ANEWARRAY - recordArrayAllocation:(Ljava/lang/String;I)V
|
||||
switch (opcode) {
|
||||
case Opcodes.NEW:
|
||||
super.visitLdcInsn(type);
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, runtimeAccounterTypeName, "recordAllocation", "(Ljava/lang/String;)V", true);
|
||||
break;
|
||||
case Opcodes.ANEWARRAY:
|
||||
super.visitInsn(Opcodes.DUP);
|
||||
super.visitLdcInsn(8);
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, runtimeAccounterTypeName, "recordArrayAllocation", "(II)V", true);
|
||||
break;
|
||||
}
|
||||
|
||||
super.visitTypeInsn(opcode, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitJumpInsn(final int opcode, final Label label) {
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, runtimeAccounterTypeName, "recordJump", "()V", true);
|
||||
super.visitJumpInsn(opcode, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits a method instruction. We add accounting information to prevent runaway
|
||||
* method calls. The case of INVOKEDYNAMIC is handled by the visitInvokeDynamicInsn
|
||||
* method, but that opcode is disallowed by the whitelisting anyway.
|
||||
*
|
||||
* @param opcode
|
||||
* @param owner
|
||||
* @param name
|
||||
* @param desc
|
||||
* @param itf
|
||||
*/
|
||||
@Override
|
||||
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
|
||||
|
||||
switch (opcode) {
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
case Opcodes.INVOKESTATIC:
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, runtimeAccounterTypeName, "recordMethodCall", "()V", itf);
|
||||
// If this is in the packages that are sandboxed, rewrite the link
|
||||
final String sandboxedOwner = Utils.sandboxInternalTypeName(owner);
|
||||
super.visitMethodInsn(opcode, sandboxedOwner, name, desc, itf);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected opcode: " + opcode + " from ASM when expecting an INVOKE");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.r3cev.visitors;
|
||||
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
/**
|
||||
* MethodVisitor that is a complete no-op. MethodVisitor is abstract, so we have to extend it
|
||||
*/
|
||||
class DefinitelyDisallowedMethodVisitor extends MethodVisitor {
|
||||
|
||||
DefinitelyDisallowedMethodVisitor(MethodVisitor baseMethodVisitor) {
|
||||
super(Opcodes.ASM5, baseMethodVisitor);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.r3cev.visitors;
|
||||
|
||||
import com.r3cev.Utils;
|
||||
import com.r3cev.WhitelistClassLoader;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This helper class visits each file (represented as a Path) in some directory
|
||||
* tree containing classes to be sandboxed.
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public final class SandboxPathVisitor extends SimpleFileVisitor<Path> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SandboxPathVisitor.class);
|
||||
|
||||
private final WhitelistClassLoader loader;
|
||||
|
||||
private final Path startFrom;
|
||||
|
||||
public SandboxPathVisitor(final WhitelistClassLoader wlcl, final Path baseDir) {
|
||||
startFrom = baseDir;
|
||||
loader = wlcl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(final Path path, final BasicFileAttributes attr) {
|
||||
// Check that this is a class file
|
||||
if (!path.toString().matches(Utils.CLASSFILE_NAME_SUFFIX)) {
|
||||
System.out.println("Skipping: "+ path);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
// Check to see if this path corresponds to an allowedClass
|
||||
final String classFileName = startFrom.relativize(path).toString().replace(".class", "");
|
||||
|
||||
if (!Utils.classShouldBeSandboxedInternal(classFileName)) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
final String nameToLoad = Utils.convertInternalFormToQualifiedClassName(classFileName);
|
||||
|
||||
try {
|
||||
loader.findClass(nameToLoad);
|
||||
} catch (ClassNotFoundException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package com.r3cev.visitors;
|
||||
|
||||
import com.r3cev.WhitelistClassLoader;
|
||||
import com.r3cev.CandidacyStatus;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.r3cev.CandidateMethod;
|
||||
import com.r3cev.Utils;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import static com.r3cev.CandidateMethod.State.*;
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
/**
|
||||
* A ASM ClassVisitor which checks classes it visits against a whitelist
|
||||
*/
|
||||
public final class WhitelistCheckingClassVisitor extends ClassVisitor {
|
||||
|
||||
private final CandidacyStatus candidacyStatus;
|
||||
private final String classname;
|
||||
private final Set<String> internalMethodNames = new HashSet<>();
|
||||
private String currentClassName;
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WhitelistCheckingClassVisitor.class);
|
||||
|
||||
public WhitelistCheckingClassVisitor(final String currentClass, final CandidacyStatus initialCandidacyStatus) {
|
||||
super(Opcodes.ASM5);
|
||||
candidacyStatus = initialCandidacyStatus;
|
||||
classname = currentClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) {
|
||||
super.visit(version, access, name, signature, superName, interfaces);
|
||||
currentClassName = name;
|
||||
|
||||
if (resolveState(Utils.convertInternalFormToQualifiedClassName(superName)) == DISALLOWED) {
|
||||
candidacyStatus.setLoadable(false);
|
||||
candidacyStatus.setReason("Superclass " + superName + " could not be loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
for (final String interfaceName : interfaces) {
|
||||
if (resolveState(Utils.convertInternalFormToQualifiedClassName(interfaceName)) == DISALLOWED) {
|
||||
candidacyStatus.setLoadable(false);
|
||||
candidacyStatus.setReason("Interface " + interfaceName + " could not be loaded");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We initially take the method passed in and store an internal representation of
|
||||
* the method signature in the our CandidacyStatus working set.
|
||||
*
|
||||
* We then get an ASM MethodVisitor (which can read the byte code of the method) and pass that to our
|
||||
* custom method visitor which perform additional checks.
|
||||
*
|
||||
* @param access
|
||||
* @param name
|
||||
* @param desc
|
||||
* @param signature
|
||||
* @param exceptions
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Visiting method with: access [" + access + "], name [" + currentClassName + "::" + name + "], signature [" + signature + "], desc ["
|
||||
+ desc + "] and exceptions [" + Arrays.toString(exceptions) + "]");
|
||||
|
||||
// Force new access control flags - for now just strictfp for deterministic
|
||||
// compliance to IEEE 754
|
||||
final int maskedAccess = access | ACC_STRICT;
|
||||
|
||||
final String internalName = classname + "." + name + ":" + desc;
|
||||
internalMethodNames.add(internalName);
|
||||
candidacyStatus.putIfAbsent(internalName);
|
||||
|
||||
final MethodVisitor baseMethodVisitor = super.visitMethod(maskedAccess, name, desc, signature, exceptions);
|
||||
|
||||
// If we're already not allowed to be loaded (a CandidateMethod was disallowed)
|
||||
// no other MethodVisitor can help
|
||||
// so return a MethodVisitor that doesn't even try to do any more work
|
||||
if (!candidacyStatus.isLoadable()) {
|
||||
// This can mask problems with the class deeper down, so disabled for now for debugging
|
||||
// return new DefinitelyDisallowedMethodVisitor(baseMethodVisitor);
|
||||
}
|
||||
|
||||
// Disallow finalizers
|
||||
if ("finalize".equals(name) && "()V".equals(desc)) {
|
||||
return new DefinitelyDisallowedMethodVisitor(baseMethodVisitor);
|
||||
}
|
||||
|
||||
// Native methods are completely disallowed
|
||||
if ((access & Opcodes.ACC_NATIVE) > 0) {
|
||||
candidacyStatus.setLoadable(false);
|
||||
candidacyStatus.setReason("Method " + internalName + " is native");
|
||||
return new DefinitelyDisallowedMethodVisitor(baseMethodVisitor);
|
||||
}
|
||||
|
||||
return new WhitelistCheckingMethodVisitor(baseMethodVisitor, candidacyStatus, internalName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Once we've finished visiting all of the methods, we check that they're all deterministic, if not we
|
||||
* tell the candidacyStatus that this is not loadable and why.
|
||||
*/
|
||||
@Override
|
||||
public void visitEnd() {
|
||||
if (!candidacyStatus.isLoadable())
|
||||
return;
|
||||
METHODS:
|
||||
for (String internalMethodName : internalMethodNames) {
|
||||
final CandidateMethod candidateMethod = candidacyStatus.getCandidateMethod(internalMethodName);
|
||||
final CandidateMethod.State candidateState = candidateMethod.getCurrentState();
|
||||
|
||||
switch (candidateState) {
|
||||
case DISALLOWED:
|
||||
candidacyStatus.setLoadable(false);
|
||||
candidacyStatus.setReason(candidateMethod.getReason());
|
||||
break METHODS;
|
||||
case DETERMINISTIC:
|
||||
break;
|
||||
case MENTIONED:
|
||||
case SCANNED:
|
||||
// Try a recursive scan (to allow multiple classes to be loaded
|
||||
// as part of the same call). The scan needs to happen on the
|
||||
// methods we *refer* to, not the current method
|
||||
for (final CandidateMethod referred : candidateMethod.getReferencedCandidateMethods()) {
|
||||
final String internalName = referred.getInternalMethodName();
|
||||
|
||||
final String toLoadQualified = Utils.convertInternalMethodNameToQualifiedClassName(internalName);
|
||||
if (!Utils.shouldAttemptToTransitivelyLoad(toLoadQualified)
|
||||
|| resolveState(toLoadQualified) == DISALLOWED) {
|
||||
referred.disallowed(internalName + " is DISALLOWED");
|
||||
candidacyStatus.setLoadable(false);
|
||||
candidacyStatus.setReason(candidateMethod.getReason());
|
||||
break METHODS;
|
||||
}
|
||||
}
|
||||
candidateMethod.deterministic();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the initial scan has produced a DISALLOWED code path
|
||||
if (!candidacyStatus.isLoadable()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the name of a class and attempts to load it using a WLCL.
|
||||
*
|
||||
* @param qualifiedClassname
|
||||
* @return
|
||||
*/
|
||||
CandidateMethod.State resolveState(final String qualifiedClassname) {
|
||||
Class<?> clz = null;
|
||||
try {
|
||||
candidacyStatus.incRecursiveCount();
|
||||
final ClassLoader loader = WhitelistClassLoader.of(candidacyStatus.getContextLoader());
|
||||
clz = loader.loadClass(qualifiedClassname);
|
||||
candidacyStatus.decRecursiveCount();
|
||||
} catch (ClassNotFoundException ex) {
|
||||
return DISALLOWED;
|
||||
}
|
||||
if (clz == null) {
|
||||
LOGGER.error("Couldn't load: " + qualifiedClassname);
|
||||
return DISALLOWED;
|
||||
}
|
||||
|
||||
return DETERMINISTIC;
|
||||
}
|
||||
|
||||
public CandidacyStatus getCandidacyStatus() {
|
||||
return candidacyStatus;
|
||||
}
|
||||
|
||||
public Set<String> getInternalMethodNames() {
|
||||
return internalMethodNames;
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
package com.r3cev.visitors;
|
||||
|
||||
import com.r3cev.CandidacyStatus;
|
||||
import com.r3cev.CandidateMethod;
|
||||
import org.objectweb.asm.Handle;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import static com.r3cev.CandidateMethod.State.*;
|
||||
import com.r3cev.Utils;
|
||||
import org.objectweb.asm.Label;
|
||||
|
||||
/**
|
||||
* A MethodVisitor which checks method instructions in order to determine if this
|
||||
* method is deterministic or not
|
||||
*
|
||||
*/
|
||||
final class WhitelistCheckingMethodVisitor extends MethodVisitor {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WhitelistCheckingMethodVisitor.class);
|
||||
|
||||
private final CandidacyStatus candidacyStatus;
|
||||
private final String currentMethodName;
|
||||
|
||||
public WhitelistCheckingMethodVisitor(final MethodVisitor methodVisitor, final CandidacyStatus initialCandidacyStatus, String methodName) {
|
||||
super(Opcodes.ASM5, methodVisitor);
|
||||
candidacyStatus = initialCandidacyStatus;
|
||||
currentMethodName = methodName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits a method instruction. A method instruction is an instruction that
|
||||
* invokes a method.
|
||||
* <p>
|
||||
* Some method instructions are by their nature un-deterministic, so we set those methods to have a
|
||||
* {@link CandidateMethod.State#DISALLOWED} State
|
||||
*/
|
||||
@Override
|
||||
public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc, final boolean itf) {
|
||||
|
||||
final CandidateMethod candidateMethod = candidacyStatus.getCandidateMethod(currentMethodName);
|
||||
final String internalName = owner + "." + name + ":" + desc;
|
||||
if (candidacyStatus.putIfAbsent(internalName)) {
|
||||
candidacyStatus.addToBacklog(internalName);
|
||||
}
|
||||
final CandidateMethod referencedCandidateMethod = candidacyStatus.getCandidateMethod(internalName);
|
||||
candidateMethod.addReferencedCandidateMethod(referencedCandidateMethod);
|
||||
|
||||
final String methodDetails = owner + " name [" + name + "], desc [" + desc + "]";
|
||||
|
||||
switch (opcode) {
|
||||
case Opcodes.INVOKEVIRTUAL:
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Visiting with INVOKEVIRTUAL: " + methodDetails);
|
||||
break;
|
||||
case Opcodes.INVOKESTATIC:
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Visiting with INVOKESTATIC: " + methodDetails);
|
||||
break;
|
||||
case Opcodes.INVOKESPECIAL:
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Visiting with INVOKESPECIAL: " + methodDetails);
|
||||
break;
|
||||
case Opcodes.INVOKEINTERFACE:
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Visiting with INVOKEINTERFACE: " + methodDetails);
|
||||
break;
|
||||
// NOTE: case Opcodes.INVOKEDYNAMIC is handled by the visitInvokeDynamicInsn call
|
||||
default:
|
||||
throw new IllegalArgumentException("Got an unexpected opcode: " + opcode + " in " + currentMethodName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type) {
|
||||
if (type == null)
|
||||
throw new IllegalArgumentException("Exception type must not be null in try/catch block in " + currentMethodName);
|
||||
|
||||
// Forcible disallow attempts to catch ThreadDeath or any throwable superclass - preserve determinism
|
||||
if (type.equals(Utils.THREAD_DEATH) || type.equals(Utils.ERROR) || type.equals(Utils.THROWABLE)) {
|
||||
final CandidateMethod candidateMethod = candidacyStatus.getCandidateMethod(currentMethodName);
|
||||
candidateMethod.disallowed("Method " + currentMethodName + " attempts to catch ThreadDeath, Error or Throwable");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently a no-op.
|
||||
*
|
||||
* The JVMspec seems to permit the possibility of using a backwards branch in a
|
||||
* tableswitch to try to create an infinite loop. However, it seems to be
|
||||
* impossible in practice - the specification of StackMapFrame seems to prevent
|
||||
* it in modern classfile formats, and even by explicitly generating a version
|
||||
* 49 (Java 5) classfile, the verifier seems to be specifically resistant to a
|
||||
* backwards branch from a tableswitch.
|
||||
*
|
||||
* We could still add a belt-and-braces static instrumentation to protect
|
||||
* against this but it currently seems unnecessary - at worse it is a branch that
|
||||
* should count against the branch limit, or an explicit disallow of a backwards
|
||||
* branch. Of course, if you find a way to exploit this, we'd welcome a pull
|
||||
* request.
|
||||
*
|
||||
* @param min
|
||||
* @param max
|
||||
* @param dflt
|
||||
* @param labels
|
||||
*/
|
||||
@Override
|
||||
public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
|
||||
super.visitTableSwitchInsn(min, max, dflt, labels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits an invokedynamic instruction - which is specifically disallowed for
|
||||
* deterministic apps.
|
||||
*
|
||||
* @param name
|
||||
* @param desc
|
||||
* @param bsm
|
||||
* @param bsmArgs
|
||||
*/
|
||||
@Override
|
||||
public void visitInvokeDynamicInsn(final String name, final String desc, final Handle bsm, final Object... bsmArgs) {
|
||||
final String methodDetails = "name [" + name + "], desc [" + desc + "]";
|
||||
final CandidateMethod candidateMethod = candidacyStatus.getCandidateMethod(currentMethodName);
|
||||
if (LOGGER.isDebugEnabled())
|
||||
LOGGER.debug("Visiting with INVOKEDYNAMIC:" + methodDetails);
|
||||
candidateMethod.disallowed("InvokeDynamic in " + currentMethodName + " with " + methodDetails);
|
||||
}
|
||||
|
||||
/**
|
||||
* If all the call instructions are deterministic for the referenced candidate methods,
|
||||
* then so is this one
|
||||
*/
|
||||
@Override
|
||||
public void visitEnd() {
|
||||
// Start from the assumption that the method is deterministic, and try to disprove
|
||||
CandidateMethod.State checkState = DETERMINISTIC;
|
||||
final CandidateMethod candidateMethod = candidacyStatus.getCandidateMethod(currentMethodName);
|
||||
if (candidateMethod == null) {
|
||||
throw new IllegalArgumentException(currentMethodName + " not found in CandidacyStatus");
|
||||
}
|
||||
if (candidateMethod.getCurrentState() == DISALLOWED) {
|
||||
return;
|
||||
}
|
||||
|
||||
CHECK:
|
||||
for (CandidateMethod referredMethod : candidateMethod.getReferencedCandidateMethods()) {
|
||||
CandidateMethod.State childMethodState = referredMethod.getCurrentState();
|
||||
switch (childMethodState) {
|
||||
case DETERMINISTIC:
|
||||
break;
|
||||
case MENTIONED:
|
||||
checkState = MENTIONED;
|
||||
break;
|
||||
case DISALLOWED:
|
||||
checkState = DISALLOWED;
|
||||
break CHECK;
|
||||
case SCANNED:
|
||||
checkState = MENTIONED;
|
||||
if (referredMethod != candidateMethod)
|
||||
throw new IllegalStateException("Illegal state of method " + referredMethod.getInternalMethodName() + " occurred when visiting method " + currentMethodName);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Illegal state occurred when visiting method " + currentMethodName);
|
||||
}
|
||||
}
|
||||
candidateMethod.setCurrentState(checkState);
|
||||
|
||||
// If this methods state hasn't already been determined, it should be set to SCANNED
|
||||
if (candidateMethod.getCurrentState() == MENTIONED)
|
||||
candidateMethod.scanned();
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package sandbox.com.r3cev.costing;
|
||||
|
||||
/**
|
||||
* A helper class that just forwards any static sandboxed calls to the real runtime
|
||||
* cost accounting class. This removes the need to special case the accounting
|
||||
* method calls during rewriting of method names
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public class RuntimeCostAccounter {
|
||||
|
||||
public static void recordJump() {
|
||||
com.r3cev.costing.RuntimeCostAccounter.recordJump();
|
||||
}
|
||||
|
||||
public static void recordAllocation(final String typeName) {
|
||||
com.r3cev.costing.RuntimeCostAccounter.recordAllocation(typeName);
|
||||
}
|
||||
|
||||
public static void recordArrayAllocation(final int length, final int multiplier) {
|
||||
com.r3cev.costing.RuntimeCostAccounter.recordArrayAllocation(length, multiplier);
|
||||
}
|
||||
|
||||
public static void recordMethodCall() {
|
||||
com.r3cev.costing.RuntimeCostAccounter.recordMethodCall();
|
||||
}
|
||||
|
||||
public static void recordThrow() {
|
||||
com.r3cev.costing.RuntimeCostAccounter.recordThrow();
|
||||
}
|
||||
|
||||
}
|
3
core/sandbox/src/main/resources/java.base.deterministic
Normal file
3
core/sandbox/src/main/resources/java.base.deterministic
Normal file
@ -0,0 +1,3 @@
|
||||
java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
java/lang/Object.getClass:()Ljava/lang/Class;
|
||||
java/lang/Object.<init>:()V
|
1
core/sandbox/src/main/resources/java.base.disallowed
Normal file
1
core/sandbox/src/main/resources/java.base.disallowed
Normal file
@ -0,0 +1 @@
|
||||
java.lang.invoke.*
|
9
core/sandbox/src/main/resources/java.base.hand-picked
Normal file
9
core/sandbox/src/main/resources/java.base.hand-picked
Normal file
@ -0,0 +1,9 @@
|
||||
java/lang/Class.getComponentType:()Ljava/lang/Class;
|
||||
java/lang/Object.equals:(Ljava/lang/Object;)Z
|
||||
java/lang/Object.getClass:()Ljava/lang/Class;
|
||||
java/lang/Object.<init>:()V
|
||||
java/lang/Throwable.fillInStackTrace:(I)Ljava/lang/Throwable;
|
||||
java/lang/reflect/Array.newArray:(Ljava/lang/Class;I)Ljava/lang/Object;
|
||||
java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
|
||||
java/lang/StringIndexOutOfBoundsException.<init>:(I)V
|
||||
java/util/Arrays.copyOfRange:([CII)[C
|
@ -0,0 +1,2 @@
|
||||
java/util/RandomAccess
|
||||
java/io/Serializable
|
1039
core/sandbox/src/main/resources/java8.scan.java.lang
Normal file
1039
core/sandbox/src/main/resources/java8.scan.java.lang
Normal file
File diff suppressed because it is too large
Load Diff
1210
core/sandbox/src/main/resources/java8.scan.java.lang_and_reflect
Normal file
1210
core/sandbox/src/main/resources/java8.scan.java.lang_and_reflect
Normal file
File diff suppressed because it is too large
Load Diff
4429
core/sandbox/src/main/resources/java8.scan.java.lang_and_util
Normal file
4429
core/sandbox/src/main/resources/java8.scan.java.lang_and_util
Normal file
File diff suppressed because it is too large
Load Diff
21
core/sandbox/src/main/resources/logback.xml
Normal file
21
core/sandbox/src/main/resources/logback.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<configuration>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>whitelistclassloader.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- daily rollover -->
|
||||
<fileNamePattern>whitelistclassloader.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
</rollingPolicy>
|
||||
|
||||
<encoder>
|
||||
<pattern>%date [%thread] %-5level %logger{35} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
@ -0,0 +1,39 @@
|
||||
package com.r3cev;
|
||||
|
||||
import com.r3cev.CandidateMethod;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Tests governing the CandidateMethod
|
||||
*/
|
||||
public class CandidateMethodTest {
|
||||
|
||||
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(CandidateMethodTest.class);
|
||||
private final static String OBJECT_INIT_METHOD = "java/lang/Object.<init>:()V";
|
||||
private final static String SYSTEM_OUT_PRINTLN = "java/io/PrintStream.println:(Ljava/lang/String;)V";
|
||||
|
||||
private CandidateMethod candidateMethod;
|
||||
|
||||
@Test
|
||||
public void given_NewCandidateMethod_when_GetState_then_StateIsUndetermined() {
|
||||
candidateMethod = CandidateMethod.of(OBJECT_INIT_METHOD);
|
||||
assertEquals(CandidateMethod.State.MENTIONED, candidateMethod.getCurrentState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_CandidateMethod_when_proven_then_StateIsDeterministic() {
|
||||
candidateMethod = CandidateMethod.proven(OBJECT_INIT_METHOD);
|
||||
assertEquals(CandidateMethod.State.DETERMINISTIC, candidateMethod.getCurrentState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_CandidateMethod_when_disallowed_then_StateIsDisallowed() {
|
||||
candidateMethod = CandidateMethod.of(SYSTEM_OUT_PRINTLN);
|
||||
candidateMethod.disallowed("dummy");
|
||||
assertEquals(CandidateMethod.State.DISALLOWED, candidateMethod.getCurrentState());
|
||||
}
|
||||
|
||||
}
|
11
core/sandbox/src/test/java/com/r3cev/Constants.java
Normal file
11
core/sandbox/src/test/java/com/r3cev/Constants.java
Normal file
@ -0,0 +1,11 @@
|
||||
package com.r3cev;
|
||||
|
||||
class Constants {
|
||||
public final static String INVALID_CLASS = "foobar";
|
||||
public final static String BASE_DETERMINISTIC_METHODS = "resource/CallObjectMethods";
|
||||
public final static String SYSTEM_OUT_PRINTLN_CLASS = "resource/CallPrintln";
|
||||
public final static String INVOKEDYNAMIC_METHOD_CLASS = "resource/UseLambdaToForceInvokeDynamic";
|
||||
public final static String NATIVE_METHOD_CLASS = "resource/CallNative";
|
||||
public final static String BASIC_COLLECTIONS_CLASS = "resource/UseBasicCollections";
|
||||
public final static String FINALIZER_CLASS = "resource/UseFinalizer";
|
||||
}
|
145
core/sandbox/src/test/java/com/r3cev/TestUtils.java
Normal file
145
core/sandbox/src/test/java/com/r3cev/TestUtils.java
Normal file
@ -0,0 +1,145 @@
|
||||
package com.r3cev;
|
||||
|
||||
import com.r3cev.costing.RuntimeCostAccounter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class TestUtils {
|
||||
|
||||
private static Path jarFSDir = null;
|
||||
private static Path tmpdir;
|
||||
|
||||
public static void setPathToTmpJar(final String resourcePathToJar) throws IOException {
|
||||
// Copy resource jar to tmp dir
|
||||
tmpdir = Files.createTempDirectory(Paths.get("/tmp"), "wlcl-tmp-test");
|
||||
final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar);
|
||||
Path copiedJar = tmpdir.resolve("tmp-resource.jar");
|
||||
Files.copy(in, copiedJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
final FileSystem fs = FileSystems.newFileSystem(copiedJar, null);
|
||||
jarFSDir = fs.getRootDirectories().iterator().next();
|
||||
}
|
||||
|
||||
public static Path copySandboxJarToTmpDir(final String resourcePathToJar) throws IOException {
|
||||
final InputStream in = TestUtils.class.getResourceAsStream(resourcePathToJar);
|
||||
Path sandboxJar = tmpdir.resolve("tmp-sandbox.jar");
|
||||
Files.copy(in, sandboxJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
final FileSystem sandboxFs = FileSystems.newFileSystem(sandboxJar, null);
|
||||
|
||||
return sandboxFs.getRootDirectories().iterator().next();
|
||||
}
|
||||
|
||||
public static Path getJarFSRoot() {
|
||||
return jarFSDir;
|
||||
}
|
||||
|
||||
public static void cleanupTmpJar() throws IOException {
|
||||
Files.walkFileTree(tmpdir, new Reaper());
|
||||
}
|
||||
|
||||
public static void checkAllCosts(final int allocCost, final int jumpCost, final int invokeCost, final int throwCost) {
|
||||
assertEquals(allocCost, RuntimeCostAccounter.getAllocationCost());
|
||||
assertEquals(jumpCost, RuntimeCostAccounter.getJumpCost());
|
||||
assertEquals(invokeCost, RuntimeCostAccounter.getInvokeCost());
|
||||
assertEquals(throwCost, RuntimeCostAccounter.getThrowCost());
|
||||
}
|
||||
|
||||
public static Class<?> transformClass(final String classFName, final int originalLength, final int newLength) throws IOException, Exception {
|
||||
byte[] basic = getBytes(classFName);
|
||||
assertEquals(originalLength, basic.length);
|
||||
final byte[] tfmd = instrumentWithCosts(basic, new HashSet<>());
|
||||
final Path testdir = Files.createTempDirectory(Paths.get("/tmp"), "greymalkin-test-");
|
||||
final Path out = testdir.resolve(classFName);
|
||||
Files.createDirectories(out.getParent());
|
||||
Files.write(out, tfmd);
|
||||
if (newLength > 0) {
|
||||
assertEquals(newLength, tfmd.length);
|
||||
}
|
||||
final MyClassloader mycl = new MyClassloader();
|
||||
final Class<?> clz = mycl.byPath(out);
|
||||
|
||||
Files.walkFileTree(testdir, new Reaper());
|
||||
|
||||
return clz;
|
||||
}
|
||||
|
||||
public static Class<?> transformClass(final String resourceMethodAccessIsRewrittenclass, int i) throws Exception {
|
||||
return transformClass(resourceMethodAccessIsRewrittenclass, i, -1);
|
||||
}
|
||||
|
||||
public static byte[] getBytes(final String original) throws IOException {
|
||||
return Files.readAllBytes(jarFSDir.resolve(original));
|
||||
}
|
||||
|
||||
// Helper for finding the correct offsets if they change
|
||||
public static void printBytes(byte[] data) {
|
||||
byte[] datum = new byte[1];
|
||||
for (int i=0; i < data.length; i++) {
|
||||
datum[0] = data[i];
|
||||
System.out.println(i +" : "+ DatatypeConverter.printHexBinary(datum));
|
||||
}
|
||||
}
|
||||
|
||||
public static int findOffset(byte[] classBytes, byte[] originalSeq) {
|
||||
int offset = 0;
|
||||
for (int i=415; i < classBytes.length; i++) {
|
||||
if (classBytes[i] != originalSeq[offset]) {
|
||||
offset = 0;
|
||||
continue;
|
||||
}
|
||||
if (offset == originalSeq.length - 1) {
|
||||
return i - offset;
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static byte[] instrumentWithCosts(byte[] basic, Set<String> hashSet) throws Exception {
|
||||
final WhitelistClassLoader wlcl = WhitelistClassLoader.of("/tmp");
|
||||
return wlcl.instrumentWithCosts(basic, hashSet);
|
||||
}
|
||||
|
||||
|
||||
public static final class MyClassloader extends ClassLoader {
|
||||
|
||||
public Class<?> byPath(Path p) throws IOException {
|
||||
final byte[] buffy = Files.readAllBytes(p);
|
||||
return defineClass(null, buffy, 0, buffy.length);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Reaper extends SimpleFileVisitor<Path> {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
if (exc == null) {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
} else {
|
||||
throw exc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
package com.r3cev;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Test;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.*;
|
||||
|
||||
public class WhitelistClassLoaderTest {
|
||||
|
||||
private static WhitelistClassLoader wlcl;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws Exception {
|
||||
TestUtils.setPathToTmpJar("/resource.jar");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setupIndividualTest() throws Exception {
|
||||
wlcl = WhitelistClassLoader.of(TestUtils.getJarFSRoot());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void shutdown() throws Exception {
|
||||
TestUtils.cleanupTmpJar();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_ValidBasicMethods_then_ClassCanBeLoaded() throws Exception {
|
||||
Class<?> clz = wlcl.loadClass("resource.CallObjectMethods");
|
||||
assertNotNull("Loaded class appears to be null", clz);
|
||||
}
|
||||
|
||||
@Test(expected = ClassNotFoundException.class)
|
||||
public void given_ValidIOMethod_then_ClassCannotBeLoaded() throws Exception {
|
||||
Class<?> clz = wlcl.loadClass("resource.CallPrintln");
|
||||
|
||||
fail("Class should not load");
|
||||
}
|
||||
|
||||
@Test(expected = ClassNotFoundException.class)
|
||||
public void given_InvokeDynamic_then_ClassCannotBeLoaded() throws Exception {
|
||||
Class<?> clz = wlcl.loadClass("resource.UseLambdaToForceInvokeDynamic");
|
||||
fail("Class should not load");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_BasicCollections_then_ClassCanBeLoaded() throws Exception {
|
||||
wlcl.addJarToSandbox(TestUtils.copySandboxJarToTmpDir("/sandbox.jar"));
|
||||
final Class<?> clz = wlcl.loadClass("resource.UseBasicCollections");
|
||||
|
||||
assertNotNull("Loaded class appears to be null", clz);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_SimpleLinkedClasses_then_ClassCanBeLoaded() throws Exception {
|
||||
Class<?> clz = wlcl.loadClass("resource.ARefersToB");
|
||||
assertNotNull("Loaded class appears to be null", clz);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_DeeplyTransitivelyLinkedClasses_then_ClassCanBeLoaded() throws Exception {
|
||||
Class<?> clz = wlcl.loadClass("transitive.Chain4701");
|
||||
assertNotNull("Loaded class appears to be null", clz);
|
||||
final Object o = clz.newInstance();
|
||||
assertNotNull("Created object appears to be null", o);
|
||||
}
|
||||
|
||||
@Test(expected = ClassNotFoundException.class)
|
||||
public void given_OverlyDeeplyTransitivelyLinkedClasses_then_ClassCanBeLoaded() throws Exception {
|
||||
Class<?> clz = wlcl.loadClass("transitive.Chain4498");
|
||||
fail("Class should not have loaded, but it did");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void foo_tesst() throws Exception {
|
||||
Class<?> clz = null;
|
||||
try {
|
||||
clz = wlcl.loadClass("transitive.Chain4498");
|
||||
} catch (final Throwable e) {
|
||||
}
|
||||
System.out.println("Handled first OK");
|
||||
assertNull(clz);
|
||||
|
||||
// RESET
|
||||
setupIndividualTest();
|
||||
// clz = wlcl.loadClass("transitive.Chain4501");
|
||||
clz = wlcl.loadClass("transitive.Chain4601");
|
||||
assertNotNull("Loaded class appears to be null", clz);
|
||||
final Object o = clz.newInstance();
|
||||
assertNotNull("Created object appears to be null", o);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_SimpleCyclicClasses_then_ClassCanBeLoaded() throws Exception {
|
||||
Class<?> clz = wlcl.loadClass("resource.ARefersToBCyclic");
|
||||
assertNotNull("Loaded class appears to be null", clz);
|
||||
final Object o = clz.newInstance();
|
||||
assertTrue("New object should be a Runnable", o instanceof Runnable);
|
||||
Runnable r = (Runnable) o;
|
||||
r.run();
|
||||
assertTrue("Execution of run failed", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void given_MultipleTransformedClasses_then_ClassCanBeLoaded() throws Exception {
|
||||
final Class<?> clz = wlcl.loadClass("resource.ObjectArrayAlloc");
|
||||
assertNotNull("ObjectArrayAlloc class could not be transformed and loaded", clz);
|
||||
final Object o = clz.newInstance();
|
||||
final Method allocObj = clz.getMethod("addEntry");
|
||||
final Object ret = allocObj.invoke(o);
|
||||
assertTrue(ret instanceof String);
|
||||
final String s = (String) ret;
|
||||
assertEquals("324Foo", s);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_test_exceptions() throws Exception {
|
||||
final Class<?> clz = wlcl.loadClass("resource.ThrowExceptions");
|
||||
assertNotNull("ThrowExceptions class could not be transformed and loaded", clz);
|
||||
}
|
||||
|
||||
|
||||
// TODO Test cases that terminate when other resource limits are broken
|
||||
@Test
|
||||
public void when_too_much_memory_is_allocated_then_thread_dies() throws Exception {
|
||||
final Class<?> clz = wlcl.loadClass("resource.LargeByteArrayAlloc");
|
||||
final AtomicBoolean executed = new AtomicBoolean(false);
|
||||
|
||||
Runnable r = () -> {
|
||||
try {
|
||||
final Object o = clz.newInstance();
|
||||
final Method allocObj = clz.getMethod("addEntry");
|
||||
final Object ret = allocObj.invoke(o);
|
||||
} catch (InvocationTargetException invx) {
|
||||
return;
|
||||
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException ex) {
|
||||
}
|
||||
executed.set(true);
|
||||
while (true) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ex) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Thread t = new Thread(r);
|
||||
t.start();
|
||||
t.join();
|
||||
// Belt and braces - did the thread die before it could flip the AtomicBoolean
|
||||
assertFalse("Executed condition should be false", executed.get());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package com.r3cev.costing;
|
||||
|
||||
import com.r3cev.TestUtils;
|
||||
import com.r3cev.WhitelistClassLoader;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import org.junit.AfterClass;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public class DeterministicClassInstrumenterTest {
|
||||
|
||||
@BeforeClass
|
||||
public static void setup_resource_jar() throws IOException, URISyntaxException {
|
||||
TestUtils.setPathToTmpJar("/resource.jar");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void kill_resource_jar() throws IOException {
|
||||
TestUtils.cleanupTmpJar();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void when_given_simple_code_it_executes() throws Exception {
|
||||
final Class<?> clz = TestUtils.transformClass("resource/CallObjectMethods.class", 525, 723);
|
||||
final Object o = clz.newInstance();
|
||||
final Method allocObj = clz.getMethod("callBasicMethodsOnObject");
|
||||
final Object ret = allocObj.invoke(o);
|
||||
assertTrue(ret instanceof Boolean);
|
||||
final Boolean s = (Boolean) ret;
|
||||
assertTrue(s);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void when_monitors_are_present_then_they_are_removed() throws Exception {
|
||||
Class<?> clz = TestUtils.transformClass("resource/SynchronizedBlock.class", 522, 712);
|
||||
Object o = clz.newInstance();
|
||||
Method allocObj = clz.getMethod("exampleBlockSynchronized");
|
||||
Object ret = allocObj.invoke(o);
|
||||
assertEquals("Synched", ret);
|
||||
|
||||
clz = TestUtils.transformClass("resource/SynchronizedMethod.class", 420, 585);
|
||||
o = clz.newInstance();
|
||||
allocObj = clz.getMethod("exampleSynchronized");
|
||||
ret = allocObj.invoke(o);
|
||||
assertEquals("SynchedMethod", ret);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void when_monitors_are_present_then_byte_stream_is_altered() throws Exception {
|
||||
// Do an actual byte check
|
||||
byte[] basic = TestUtils.getBytes("resource/SynchronizedBlock.class");
|
||||
final byte[] tfmd = TestUtils.instrumentWithCosts(basic, new HashSet<>());
|
||||
|
||||
// -62 is really 0xc2 but signed bytes in Java :(
|
||||
final byte[] originalSeq = {0x2a, 0x59, 0x4c, -62, 0x12, 0x02, 0x2b, -61};
|
||||
final byte[] tfmdSeq = {0x2a, 0x59, 0x4c, 0x57, 0x12, 0x02, 0x2b, 0x57};
|
||||
|
||||
// TestUtils.printBytes(basic);
|
||||
final int origOffset = TestUtils.findOffset(basic, originalSeq);
|
||||
final int tmfdOffset = TestUtils.findOffset(tfmd, tfmdSeq);
|
||||
|
||||
for (int i = 0; i < originalSeq.length; i++) {
|
||||
assertEquals(originalSeq[i], basic[origOffset + i]);
|
||||
assertEquals(tfmdSeq[i], tfmd[tmfdOffset + i]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package com.r3cev.costing;
|
||||
|
||||
import com.r3cev.TestUtils;
|
||||
import static com.r3cev.TestUtils.*;
|
||||
import com.r3cev.Utils;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URISyntaxException;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
public class SandboxedRewritingTest {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
RuntimeCostAccounter.resetCounters();
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setup_resource_jar() throws IOException, URISyntaxException {
|
||||
TestUtils.setPathToTmpJar("/resource.jar");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void kill_resource_jar() throws IOException {
|
||||
TestUtils.cleanupTmpJar();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRewriting() {
|
||||
String rewritten = Utils.sandboxInternalTypeName("java/lang/Object");
|
||||
assertEquals("Expected Object::equals to be unchanged, but it was: " + rewritten, "java/lang/Object", rewritten);
|
||||
rewritten = Utils.sandboxInternalTypeName("java/util/ArrayList");
|
||||
assertEquals("Expected ArrayList::new to be sandboxed, but it was: " + rewritten, "sandbox/java/util/ArrayList", rewritten);
|
||||
rewritten = Utils.sandboxInternalTypeName("sandbox/java/util/ArrayList");
|
||||
assertEquals("Expected sandboxed ArrayList::new to be left unchanged, but it was: " + rewritten, "sandbox/java/util/ArrayList", rewritten);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void when_desc_is_provided_it_is_correctly_rewritten() {
|
||||
final String voidSig = "()V";
|
||||
final String rwVoidSig = Utils.rewriteDescInternal(voidSig);
|
||||
assertEquals("Expected " + voidSig + " to be unchanged, but it was: " + rwVoidSig, rwVoidSig, voidSig);
|
||||
|
||||
final String primSig = "(IIJ)Z";
|
||||
final String rwPrimSig = Utils.rewriteDescInternal(primSig);
|
||||
assertEquals("Expected " + primSig + " to be unchanged, but it was: " + rwPrimSig, rwPrimSig, primSig);
|
||||
|
||||
final String toStringSig = "()Ljava/lang/String;";
|
||||
final String rwToStringSig = Utils.rewriteDescInternal(toStringSig);
|
||||
assertEquals("Expected " + toStringSig + " to be unchanged, but it was: " + rwToStringSig, rwToStringSig, toStringSig);
|
||||
|
||||
final String listGetterSig = "()Ljava/util/ArrayList;";
|
||||
final String exListGetterSig = "()Lsandbox/java/util/ArrayList;";
|
||||
final String rwListGetterSig = Utils.rewriteDescInternal(listGetterSig);
|
||||
assertEquals("Expected " + listGetterSig + " to be " + exListGetterSig + ", but it was: " + rwListGetterSig, exListGetterSig, rwListGetterSig);
|
||||
|
||||
final String sandboxListGetterSig = "()Lsandbox/java/util/ArrayList;";
|
||||
final String rwSandboxListGetterSig = Utils.rewriteDescInternal(sandboxListGetterSig);
|
||||
assertEquals("Expected " + sandboxListGetterSig + " to be unchanged, but it was: " + rwSandboxListGetterSig, sandboxListGetterSig, rwSandboxListGetterSig);
|
||||
|
||||
final String twoSig = "(Ljava/util/HashMap;)Ljava/util/Set;";
|
||||
final String exTwoSig = "(Lsandbox/java/util/HashMap;)Lsandbox/java/util/Set;";
|
||||
final String rwTwoSig = Utils.rewriteDescInternal(twoSig);
|
||||
assertEquals("Expected " + twoSig + " to be " + exTwoSig + ", but it was: " + rwTwoSig, exTwoSig, rwTwoSig);
|
||||
|
||||
final String arrSig = "(Ljava/util/HashMap;)[Ljava/util/Set;";
|
||||
final String exArrSig = "(Lsandbox/java/util/HashMap;)[Lsandbox/java/util/Set;";
|
||||
final String rwArrSig = Utils.rewriteDescInternal(arrSig);
|
||||
assertEquals("Expected " + arrSig + " to be " + exArrSig + ", but it was: " + rwArrSig, exArrSig, rwArrSig);
|
||||
|
||||
final String compArrSig = "([[IJLjava/util/HashMap;)[[Ljava/util/Set;";
|
||||
final String exCompArrSig = "([[IJLsandbox/java/util/HashMap;)[[Lsandbox/java/util/Set;";
|
||||
final String rwCompArrSig = Utils.rewriteDescInternal(compArrSig);
|
||||
assertEquals("Expected " + compArrSig + " to be " + exCompArrSig + ", but it was: " + rwCompArrSig, exCompArrSig, rwCompArrSig);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actually_rewrite_a_method_access_and_check_it_works_as_expected() throws Exception {
|
||||
final Class<?> clz = transformClass("resource/MethodAccessIsRewritten.class", 412);
|
||||
final String className = clz.getName();
|
||||
assertEquals("Incorrect rewritten class name: ", "sandbox.resource.MethodAccessIsRewritten", className);
|
||||
final Object o = clz.newInstance();
|
||||
final Method m = clz.getMethod("makeObject");
|
||||
final Object ret = m.invoke(o);
|
||||
assertTrue(Object.class == ret.getClass());
|
||||
checkAllCosts(1, 0, 2, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actually_rewrite_calls_to_object_methods() throws Exception {
|
||||
final Class<?> clz = transformClass("resource/CallObjectMethods.class", 525);
|
||||
final String className = clz.getName();
|
||||
assertEquals("Incorrect rewritten class name: ", "sandbox.resource.CallObjectMethods", className);
|
||||
final Object o = clz.newInstance();
|
||||
final Method m = clz.getMethod("callBasicMethodsOnObject");
|
||||
final Object ret = m.invoke(o);
|
||||
assertTrue(Boolean.class == ret.getClass());
|
||||
assertTrue((Boolean) ret);
|
||||
checkAllCosts(0, 2, 3, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void check_primitive_array_allocation() throws Exception {
|
||||
final Class<?> clz = transformClass("resource/SimpleArrayAlloc.class", 727);
|
||||
final String className = clz.getName();
|
||||
assertEquals("Incorrect rewritten class name: ", "sandbox.resource.SimpleArrayAlloc", className);
|
||||
final Method m = clz.getMethod("allocPrimitiveArrays");
|
||||
final Object ret = m.invoke(null);
|
||||
assertNull(ret);
|
||||
checkAllCosts(778, 1, 0, 0);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package sandbox.greymalkin;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author ben
|
||||
*/
|
||||
// Simple hack for now, generalise to lambdas later...
|
||||
public interface StringReturner {
|
||||
|
||||
String addEntry();
|
||||
}
|
BIN
core/sandbox/src/test/resources/java/lang/Exception.class
Normal file
BIN
core/sandbox/src/test/resources/java/lang/Exception.class
Normal file
Binary file not shown.
BIN
core/sandbox/src/test/resources/java/lang/Throwable.class
Normal file
BIN
core/sandbox/src/test/resources/java/lang/Throwable.class
Normal file
Binary file not shown.
BIN
core/sandbox/src/test/resources/java/lang/reflect/Array.class
Normal file
BIN
core/sandbox/src/test/resources/java/lang/reflect/Array.class
Normal file
Binary file not shown.
BIN
core/sandbox/src/test/resources/java/util/ArrayList.class
Normal file
BIN
core/sandbox/src/test/resources/java/util/ArrayList.class
Normal file
Binary file not shown.
BIN
core/sandbox/src/test/resources/java/util/Arrays.class
Normal file
BIN
core/sandbox/src/test/resources/java/util/Arrays.class
Normal file
Binary file not shown.
29
core/sandbox/src/test/resources/logback-test.xml
Normal file
29
core/sandbox/src/test/resources/logback-test.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<configuration>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<Pattern>%d %-4relative [%thread] %-5level %logger{35} - %msg%n</Pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>whitelistclassloader-test.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- daily rollover -->
|
||||
<fileNamePattern>whitelistclassloader-test.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
</rollingPolicy>
|
||||
|
||||
<encoder>
|
||||
<pattern>%date [%thread] %-5level %logger{35} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
BIN
core/sandbox/src/test/resources/resource.jar
Normal file
BIN
core/sandbox/src/test/resources/resource.jar
Normal file
Binary file not shown.
BIN
core/sandbox/src/test/resources/sandbox.jar
Normal file
BIN
core/sandbox/src/test/resources/sandbox.jar
Normal file
Binary file not shown.
@ -11,6 +11,7 @@ include 'test-utils'
|
||||
include 'tools:explorer'
|
||||
include 'tools:loadtest'
|
||||
include 'docs/source/example-code' // Note that we are deliberately choosing to use '/' here. With ':' gradle would treat the directories as actual projects.
|
||||
include 'core:sandbox'
|
||||
include 'samples:attachment-demo'
|
||||
include 'samples:trader-demo'
|
||||
include 'samples:irs-demo'
|
||||
|
Loading…
x
Reference in New Issue
Block a user