Import sandbox code developed by Ben Evans with review and contributions from myself.

This commit is contained in:
Mike Hearn 2016-11-16 11:18:00 +01:00
parent fe5df11b23
commit 2f02e56893
43 changed files with 9396 additions and 0 deletions

20
core/sandbox/README.md Normal file
View 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
View 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'
}

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

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

View File

@ -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('.', '/');
}
}
}

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

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

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@ -0,0 +1 @@
java.lang.invoke.*

View 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

View File

@ -0,0 +1,2 @@
java/util/RandomAccess
java/io/Serializable

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
package sandbox.greymalkin;
/**
*
* @author ben
*/
// Simple hack for now, generalise to lambdas later...
public interface StringReturner {
String addEntry();
}

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

View File

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