mirror of
https://github.com/corda/corda.git
synced 2025-06-01 07:00:54 +00:00
OS->Ent merge
This commit is contained in:
commit
63ae29d153
@ -610,7 +610,8 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
assertThat(json["serialNumber"].bigIntegerValue()).isEqualTo(cert.serialNumber)
|
||||
assertThat(json["issuer"].valueAs<X500Principal>(mapper)).isEqualTo(cert.issuerX500Principal)
|
||||
assertThat(json["subject"].valueAs<X500Principal>(mapper)).isEqualTo(cert.subjectX500Principal)
|
||||
assertThat(json["publicKey"].valueAs<PublicKey>(mapper)).isEqualTo(cert.publicKey)
|
||||
// cert.publicKey should be converted to a supported format (this is required because [Certificate] returns keys as SUN EC keys, not BC).
|
||||
assertThat(json["publicKey"].valueAs<PublicKey>(mapper)).isEqualTo(Crypto.toSupportedPublicKey(cert.publicKey))
|
||||
assertThat(json["notAfter"].valueAs<Date>(mapper)).isEqualTo(cert.notAfter)
|
||||
assertThat(json["notBefore"].valueAs<Date>(mapper)).isEqualTo(cert.notBefore)
|
||||
assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded)
|
||||
|
@ -11,6 +11,7 @@ import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi
|
||||
import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
||||
@ -30,6 +31,8 @@ internal val cordaBouncyCastleProvider = BouncyCastleProvider().apply {
|
||||
override fun generatePublic(keyInfo: SubjectPublicKeyInfo) = decodePublicKey(EDDSA_ED25519_SHA512, keyInfo.encoded)
|
||||
override fun generatePrivate(keyInfo: PrivateKeyInfo) = decodePrivateKey(EDDSA_ED25519_SHA512, keyInfo.encoded)
|
||||
})
|
||||
// Required due to [X509CRL].verify() reported issues in network-services after BC 1.60 update.
|
||||
put("AlgorithmParameters.SHA256WITHECDSA", AlgorithmParametersSpi::class.java.name)
|
||||
}.also {
|
||||
// This registration is needed for reading back EdDSA key from java keystore.
|
||||
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
|
||||
|
@ -40,10 +40,11 @@ data class CordappImpl(
|
||||
override val cordappClasses: List<String> = (rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames
|
||||
|
||||
// TODO Why a seperate Info class and not just have the fields directly in CordappImpl?
|
||||
data class Info(val shortName: String, val vendor: String, val version: String) {
|
||||
data class Info(val shortName: String, val vendor: String, val version: String, val minimumPlatformVersion: Int, val targetPlatformVersion: Int) {
|
||||
companion object {
|
||||
private const val UNKNOWN_VALUE = "Unknown"
|
||||
val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE)
|
||||
|
||||
val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE, 1, 1)
|
||||
}
|
||||
|
||||
fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, version).any { it == UNKNOWN_VALUE }
|
||||
|
@ -0,0 +1,65 @@
|
||||
package net.corda.core.internal.cordapp
|
||||
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Provides a way to acquire information about the calling CorDapp.
|
||||
*/
|
||||
object CordappInfoResolver {
|
||||
private val logger = loggerFor<CordappInfoResolver>()
|
||||
private val cordappClasses: ConcurrentHashMap<String, Set<CordappImpl.Info>> = ConcurrentHashMap()
|
||||
|
||||
// TODO use the StackWalker API once we migrate to Java 9+
|
||||
private var cordappInfoResolver: () -> CordappImpl.Info? = {
|
||||
Exception().stackTrace
|
||||
.mapNotNull { cordappClasses[it.className] }
|
||||
// If there is more than one cordapp registered for a class name we can't determine the "correct" one and return null.
|
||||
.firstOrNull { it.size < 2 }?.single()
|
||||
}
|
||||
|
||||
/*
|
||||
* Associates class names with CorDapps or logs a warning when a CorDapp is already registered for a given class.
|
||||
* This could happen when trying to run different versions of the same CorDapp on the same node.
|
||||
*/
|
||||
@Synchronized
|
||||
fun register(classes: List<String>, cordapp: CordappImpl.Info) {
|
||||
classes.forEach {
|
||||
if (cordappClasses.containsKey(it)) {
|
||||
logger.warn("More than one CorDapp registered for $it.")
|
||||
cordappClasses[it] = cordappClasses[it]!! + cordapp
|
||||
} else {
|
||||
cordappClasses[it] = setOf(cordapp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This should only be used when making a change that would break compatibility with existing CorDapps. The change
|
||||
* can then be version-gated, meaning the old behaviour is used if the calling CorDapp's target version is lower
|
||||
* than the platform version that introduces the new behaviour.
|
||||
* In situations where a `[CordappProvider]` is available the CorDapp context should be obtained from there.
|
||||
*
|
||||
* @return Information about the CorDapp from which the invoker is called, null if called outside a CorDapp or the
|
||||
* calling CorDapp cannot be reliably determined..
|
||||
*/
|
||||
fun getCorDappInfo(): CordappImpl.Info? = cordappInfoResolver()
|
||||
|
||||
/**
|
||||
* Temporarily switch out the internal resolver for another one. For use in testing.
|
||||
*/
|
||||
@Synchronized
|
||||
fun withCordappInfoResolution(tempResolver: () -> CordappImpl.Info?, block: () -> Unit) {
|
||||
val resolver = cordappInfoResolver
|
||||
cordappInfoResolver = tempResolver
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
cordappInfoResolver = resolver
|
||||
}
|
||||
}
|
||||
|
||||
internal fun clear() {
|
||||
cordappClasses.clear()
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package net.corda.core.internal.cordapp
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CordappInfoResolverTest {
|
||||
|
||||
@Before
|
||||
@After
|
||||
fun clearCordappInfoResolver() {
|
||||
CordappInfoResolver.clear()
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `The correct cordapp resolver is used after calling withCordappResolution`() {
|
||||
val defaultTargetVersion = 222
|
||||
|
||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, defaultTargetVersion))
|
||||
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
|
||||
|
||||
val expectedTargetVersion = 555
|
||||
CordappInfoResolver.withCordappInfoResolution( { CordappImpl.Info("foo", "bar", "1", 2, expectedTargetVersion) })
|
||||
{
|
||||
val actualTargetVersion = returnCallingTargetVersion()
|
||||
assertEquals(expectedTargetVersion, actualTargetVersion)
|
||||
}
|
||||
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `When more than one cordapp is registered for the same class, the resolver returns null`() {
|
||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, 222))
|
||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test1", "test1", "1", 2, 456))
|
||||
assertEquals(0, returnCallingTargetVersion())
|
||||
}
|
||||
|
||||
private fun returnCallingTargetVersion(): Int {
|
||||
return CordappInfoResolver.getCorDappInfo()?.targetPlatformVersion ?: 0
|
||||
}
|
||||
}
|
@ -3,16 +3,27 @@ plugins {
|
||||
}
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
apply plugin: 'idea'
|
||||
|
||||
description 'Corda deterministic JVM sandbox'
|
||||
|
||||
ext {
|
||||
// Shaded version of ASM to avoid conflict with root project.
|
||||
asm_version = '6.1.1'
|
||||
asm_version = '6.2.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "$artifactory_contextUrl/corda-dev"
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
testCompile.extendsFrom shadow
|
||||
jdkRt.resolutionStrategy {
|
||||
// Always check the repository for a newer SNAPSHOT.
|
||||
cacheChangingModulesFor 0, 'seconds'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -32,6 +43,7 @@ dependencies {
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||
testCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
jdkRt "net.corda:deterministic-rt:latest.integration"
|
||||
}
|
||||
|
||||
jar.enabled = false
|
||||
@ -43,6 +55,10 @@ shadowJar {
|
||||
}
|
||||
assemble.dependsOn shadowJar
|
||||
|
||||
tasks.withType(Test) {
|
||||
systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath
|
||||
}
|
||||
|
||||
artifacts {
|
||||
publish shadowJar
|
||||
}
|
||||
@ -51,3 +67,10 @@ publish {
|
||||
dependenciesFrom configurations.shadow
|
||||
name shadowJar.baseName
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ abstract class ClassCommand : CommandBase() {
|
||||
|
||||
private lateinit var classLoader: ClassLoader
|
||||
|
||||
protected var executor = SandboxExecutor<Any, Any>()
|
||||
protected var executor = SandboxExecutor<Any, Any>(SandboxConfiguration.DEFAULT)
|
||||
|
||||
private var derivedWhitelist: Whitelist = Whitelist.MINIMAL
|
||||
|
||||
@ -114,7 +114,7 @@ abstract class ClassCommand : CommandBase() {
|
||||
}
|
||||
|
||||
private fun findDiscoverableRunnables(filters: Array<String>): List<Class<*>> {
|
||||
val classes = find<DiscoverableRunnable>()
|
||||
val classes = find<java.util.function.Function<*,*>>()
|
||||
val applicableFilters = filters
|
||||
.filter { !isJarFile(it) && !isFullClassName(it) }
|
||||
val filteredClasses = applicableFilters
|
||||
@ -125,7 +125,7 @@ abstract class ClassCommand : CommandBase() {
|
||||
}
|
||||
|
||||
if (applicableFilters.isNotEmpty() && filteredClasses.isEmpty()) {
|
||||
throw Exception("Could not find any classes implementing ${SandboxedRunnable::class.java.simpleName} " +
|
||||
throw Exception("Could not find any classes implementing ${java.util.function.Function::class.java.simpleName} " +
|
||||
"whose name matches '${applicableFilters.joinToString(" ")}'")
|
||||
}
|
||||
|
||||
@ -189,7 +189,7 @@ abstract class ClassCommand : CommandBase() {
|
||||
profile = profile,
|
||||
rules = if (ignoreRules) { emptyList() } else { Discovery.find() },
|
||||
emitters = ignoreEmitters.emptyListIfTrueOtherwiseNull(),
|
||||
definitionProviders = if(ignoreDefinitionProviders) { emptyList() } else { Discovery.find() },
|
||||
definitionProviders = if (ignoreDefinitionProviders) { emptyList() } else { Discovery.find() },
|
||||
enableTracing = !disableTracing,
|
||||
analysisConfiguration = AnalysisConfiguration(
|
||||
whitelist = whitelist,
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.djvm.tools.cli
|
||||
|
||||
import net.corda.djvm.execution.SandboxedRunnable
|
||||
import net.corda.djvm.source.ClassSource
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.Parameters
|
||||
@ -20,7 +19,7 @@ class RunCommand : ClassCommand() {
|
||||
var classes: Array<String> = emptyArray()
|
||||
|
||||
override fun processClasses(classes: List<Class<*>>) {
|
||||
val interfaceName = SandboxedRunnable::class.java.simpleName
|
||||
val interfaceName = java.util.function.Function::class.java.simpleName
|
||||
for (clazz in classes) {
|
||||
if (!clazz.interfaces.any { it.simpleName == interfaceName }) {
|
||||
printError("Class is not an instance of $interfaceName; ${clazz.name}")
|
||||
|
@ -33,52 +33,53 @@ class WhitelistGenerateCommand : CommandBase() {
|
||||
override fun validateArguments() = paths.isNotEmpty()
|
||||
|
||||
override fun handleCommand(): Boolean {
|
||||
val entries = mutableListOf<String>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
override fun visitClass(clazz: ClassRepresentation): ClassRepresentation {
|
||||
entries.add(clazz.name)
|
||||
return super.visitClass(clazz)
|
||||
}
|
||||
val entries = AnalysisConfiguration().use { configuration ->
|
||||
val entries = mutableListOf<String>()
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitClass(clazz: ClassRepresentation): ClassRepresentation {
|
||||
entries.add(clazz.name)
|
||||
return super.visitClass(clazz)
|
||||
}
|
||||
|
||||
override fun visitMethod(clazz: ClassRepresentation, method: Member): Member {
|
||||
visitMember(clazz, method)
|
||||
return super.visitMethod(clazz, method)
|
||||
}
|
||||
override fun visitMethod(clazz: ClassRepresentation, method: Member): Member {
|
||||
visitMember(clazz, method)
|
||||
return super.visitMethod(clazz, method)
|
||||
}
|
||||
|
||||
override fun visitField(clazz: ClassRepresentation, field: Member): Member {
|
||||
visitMember(clazz, field)
|
||||
return super.visitField(clazz, field)
|
||||
}
|
||||
override fun visitField(clazz: ClassRepresentation, field: Member): Member {
|
||||
visitMember(clazz, field)
|
||||
return super.visitField(clazz, field)
|
||||
}
|
||||
|
||||
private fun visitMember(clazz: ClassRepresentation, member: Member) {
|
||||
entries.add("${clazz.name}.${member.memberName}:${member.signature}")
|
||||
private fun visitMember(clazz: ClassRepresentation, member: Member) {
|
||||
entries.add("${clazz.name}.${member.memberName}:${member.signature}")
|
||||
}
|
||||
}
|
||||
val context = AnalysisContext.fromConfiguration(configuration)
|
||||
for (path in paths) {
|
||||
ClassSource.fromPath(path).getStreamIterator().forEach {
|
||||
visitor.analyze(it, context)
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
val context = AnalysisContext.fromConfiguration(AnalysisConfiguration(), emptyList())
|
||||
for (path in paths) {
|
||||
ClassSource.fromPath(path).getStreamIterator().forEach {
|
||||
visitor.analyze(it, context)
|
||||
}
|
||||
}
|
||||
val output = output
|
||||
if (output != null) {
|
||||
Files.newOutputStream(output, StandardOpenOption.CREATE).use {
|
||||
GZIPOutputStream(it).use {
|
||||
PrintStream(it).use {
|
||||
it.println("""
|
||||
output?.also {
|
||||
Files.newOutputStream(it, StandardOpenOption.CREATE).use { out ->
|
||||
GZIPOutputStream(out).use { gzip ->
|
||||
PrintStream(gzip).use { pout ->
|
||||
pout.println("""
|
||||
|java/.*
|
||||
|javax/.*
|
||||
|jdk/.*
|
||||
|com/sun/.*
|
||||
|sun/.*
|
||||
|---
|
||||
""".trimMargin().trim())
|
||||
printEntries(it, entries)
|
||||
printEntries(pout, entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printEntries(System.out, entries)
|
||||
}
|
||||
} ?: printEntries(System.out, entries)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -3,25 +3,20 @@ package net.corda.djvm
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.costing.RuntimeCostSummary
|
||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||
import net.corda.djvm.source.ClassSource
|
||||
|
||||
/**
|
||||
* The context in which a sandboxed operation is run.
|
||||
*
|
||||
* @property configuration The configuration of the sandbox.
|
||||
* @property inputClasses The classes passed in for analysis.
|
||||
*/
|
||||
class SandboxRuntimeContext(
|
||||
val configuration: SandboxConfiguration,
|
||||
private val inputClasses: List<ClassSource>
|
||||
) {
|
||||
class SandboxRuntimeContext(val configuration: SandboxConfiguration) {
|
||||
|
||||
/**
|
||||
* The class loader to use inside the sandbox.
|
||||
*/
|
||||
val classLoader: SandboxClassLoader = SandboxClassLoader(
|
||||
configuration,
|
||||
AnalysisContext.fromConfiguration(configuration.analysisConfiguration, inputClasses)
|
||||
AnalysisContext.fromConfiguration(configuration.analysisConfiguration)
|
||||
)
|
||||
|
||||
/**
|
||||
@ -35,7 +30,7 @@ class SandboxRuntimeContext(
|
||||
fun use(action: SandboxRuntimeContext.() -> Unit) {
|
||||
SandboxRuntimeContext.instance = this
|
||||
try {
|
||||
this.action()
|
||||
action(this)
|
||||
} finally {
|
||||
threadLocalContext.remove()
|
||||
}
|
||||
@ -43,9 +38,7 @@ class SandboxRuntimeContext(
|
||||
|
||||
companion object {
|
||||
|
||||
private val threadLocalContext = object : ThreadLocal<SandboxRuntimeContext?>() {
|
||||
override fun initialValue(): SandboxRuntimeContext? = null
|
||||
}
|
||||
private val threadLocalContext = ThreadLocal<SandboxRuntimeContext?>()
|
||||
|
||||
/**
|
||||
* When called from within a sandbox, this returns the context for the current sandbox thread.
|
||||
|
@ -1,9 +1,15 @@
|
||||
package net.corda.djvm.analysis
|
||||
|
||||
import net.corda.djvm.code.ruleViolationError
|
||||
import net.corda.djvm.code.thresholdViolationError
|
||||
import net.corda.djvm.messages.Severity
|
||||
import net.corda.djvm.references.ClassModule
|
||||
import net.corda.djvm.references.MemberModule
|
||||
import net.corda.djvm.source.BootstrapClassLoader
|
||||
import net.corda.djvm.source.SourceClassLoader
|
||||
import sandbox.net.corda.djvm.costing.RuntimeCostAccounter
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
@ -13,7 +19,8 @@ import java.nio.file.Path
|
||||
* @param additionalPinnedClasses Classes that have already been declared in the sandbox namespace and that should be
|
||||
* made available inside the sandboxed environment.
|
||||
* @property minimumSeverityLevel The minimum severity level to log and report.
|
||||
* @property classPath The extended class path to use for the analysis.
|
||||
* @param classPath The extended class path to use for the analysis.
|
||||
* @param bootstrapJar The location of a jar containing the Java APIs.
|
||||
* @property analyzeAnnotations Analyze annotations despite not being explicitly referenced.
|
||||
* @property prefixFilters Only record messages where the originating class name matches one of the provided prefixes.
|
||||
* If none are provided, all messages will be reported.
|
||||
@ -24,32 +31,47 @@ class AnalysisConfiguration(
|
||||
val whitelist: Whitelist = Whitelist.MINIMAL,
|
||||
additionalPinnedClasses: Set<String> = emptySet(),
|
||||
val minimumSeverityLevel: Severity = Severity.WARNING,
|
||||
val classPath: List<Path> = emptyList(),
|
||||
classPath: List<Path> = emptyList(),
|
||||
bootstrapJar: Path? = null,
|
||||
val analyzeAnnotations: Boolean = false,
|
||||
val prefixFilters: List<String> = emptyList(),
|
||||
val classModule: ClassModule = ClassModule(),
|
||||
val memberModule: MemberModule = MemberModule()
|
||||
) {
|
||||
) : Closeable {
|
||||
|
||||
/**
|
||||
* Classes that have already been declared in the sandbox namespace and that should be made
|
||||
* available inside the sandboxed environment.
|
||||
*/
|
||||
val pinnedClasses: Set<String> = setOf(SANDBOXED_OBJECT, RUNTIME_COST_ACCOUNTER) + additionalPinnedClasses
|
||||
val pinnedClasses: Set<String> = setOf(
|
||||
SANDBOXED_OBJECT,
|
||||
RuntimeCostAccounter.TYPE_NAME,
|
||||
ruleViolationError,
|
||||
thresholdViolationError
|
||||
) + additionalPinnedClasses
|
||||
|
||||
/**
|
||||
* Functionality used to resolve the qualified name and relevant information about a class.
|
||||
*/
|
||||
val classResolver: ClassResolver = ClassResolver(pinnedClasses, whitelist, SANDBOX_PREFIX)
|
||||
|
||||
private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) }
|
||||
val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader)
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
supportingClassLoader.use {
|
||||
bootstrapClassLoader?.close()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The package name prefix to use for classes loaded into a sandbox.
|
||||
*/
|
||||
private const val SANDBOX_PREFIX: String = "sandbox/"
|
||||
|
||||
private const val SANDBOXED_OBJECT = "sandbox/java/lang/Object"
|
||||
private const val RUNTIME_COST_ACCOUNTER = RuntimeCostAccounter.TYPE_NAME
|
||||
private const val SANDBOXED_OBJECT = SANDBOX_PREFIX + "java/lang/Object"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package net.corda.djvm.analysis
|
||||
|
||||
import net.corda.djvm.code.asPackagePath
|
||||
import net.corda.djvm.messages.MessageCollection
|
||||
import net.corda.djvm.references.ClassHierarchy
|
||||
import net.corda.djvm.references.EntityReference
|
||||
import net.corda.djvm.references.ReferenceMap
|
||||
import net.corda.djvm.source.ClassSource
|
||||
|
||||
/**
|
||||
* The context in which one or more classes are analysed.
|
||||
@ -13,13 +13,11 @@ import net.corda.djvm.source.ClassSource
|
||||
* @property classes List of class definitions that have been analyzed.
|
||||
* @property references A collection of all referenced members found during analysis together with the locations from
|
||||
* where each member has been accessed or invoked.
|
||||
* @property inputClasses The classes passed in for analysis.
|
||||
*/
|
||||
class AnalysisContext private constructor(
|
||||
val messages: MessageCollection,
|
||||
val classes: ClassHierarchy,
|
||||
val references: ReferenceMap,
|
||||
val inputClasses: List<ClassSource>
|
||||
val references: ReferenceMap
|
||||
) {
|
||||
|
||||
private val origins = mutableMapOf<String, MutableSet<EntityReference>>()
|
||||
@ -28,7 +26,7 @@ class AnalysisContext private constructor(
|
||||
* Record a class origin in the current analysis context.
|
||||
*/
|
||||
fun recordClassOrigin(name: String, origin: EntityReference) {
|
||||
origins.getOrPut(name.normalize()) { mutableSetOf() }.add(origin)
|
||||
origins.getOrPut(name.asPackagePath) { mutableSetOf() }.add(origin)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,20 +40,14 @@ class AnalysisContext private constructor(
|
||||
/**
|
||||
* Create a new analysis context from provided configuration.
|
||||
*/
|
||||
fun fromConfiguration(configuration: AnalysisConfiguration, classes: List<ClassSource>): AnalysisContext {
|
||||
fun fromConfiguration(configuration: AnalysisConfiguration): AnalysisContext {
|
||||
return AnalysisContext(
|
||||
MessageCollection(configuration.minimumSeverityLevel, configuration.prefixFilters),
|
||||
ClassHierarchy(configuration.classModule, configuration.memberModule),
|
||||
ReferenceMap(configuration.classModule),
|
||||
classes
|
||||
ReferenceMap(configuration.classModule)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Local extension method for normalizing a class name.
|
||||
*/
|
||||
private fun String.normalize() = this.replace("/", ".")
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -4,30 +4,25 @@ import net.corda.djvm.code.EmitterModule
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.instructions.*
|
||||
import net.corda.djvm.messages.Message
|
||||
import net.corda.djvm.references.ClassReference
|
||||
import net.corda.djvm.references.ClassRepresentation
|
||||
import net.corda.djvm.references.Member
|
||||
import net.corda.djvm.references.MemberReference
|
||||
import net.corda.djvm.source.SourceClassLoader
|
||||
import net.corda.djvm.references.*
|
||||
import org.objectweb.asm.*
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Functionality for traversing a class and its members.
|
||||
*
|
||||
* @property classVisitor Class visitor to use when traversing the structure of classes.
|
||||
* @property configuration The configuration to use for the analysis
|
||||
* @property classVisitor Class visitor to use when traversing the structure of classes.
|
||||
*/
|
||||
open class ClassAndMemberVisitor(
|
||||
private val classVisitor: ClassVisitor? = null,
|
||||
private val configuration: AnalysisConfiguration = AnalysisConfiguration()
|
||||
private val configuration: AnalysisConfiguration,
|
||||
private val classVisitor: ClassVisitor?
|
||||
) {
|
||||
|
||||
/**
|
||||
* Holds a reference to the currently used analysis context.
|
||||
*/
|
||||
protected var analysisContext: AnalysisContext =
|
||||
AnalysisContext.fromConfiguration(configuration, emptyList())
|
||||
protected var analysisContext: AnalysisContext = AnalysisContext.fromConfiguration(configuration)
|
||||
|
||||
/**
|
||||
* Holds a link to the class currently being traversed.
|
||||
@ -44,12 +39,6 @@ open class ClassAndMemberVisitor(
|
||||
*/
|
||||
private var sourceLocation = SourceLocation()
|
||||
|
||||
/**
|
||||
* The class loader used to find classes on the extended class path.
|
||||
*/
|
||||
private val supportingClassLoader =
|
||||
SourceClassLoader(configuration.classPath, configuration.classResolver)
|
||||
|
||||
/**
|
||||
* Analyze class by using the provided qualified name of the class.
|
||||
*/
|
||||
@ -63,7 +52,7 @@ open class ClassAndMemberVisitor(
|
||||
* @param origin The originating class for the analysis.
|
||||
*/
|
||||
fun analyze(className: String, context: AnalysisContext, origin: String? = null) {
|
||||
supportingClassLoader.classReader(className, context, origin).apply {
|
||||
configuration.supportingClassLoader.classReader(className, context, origin).apply {
|
||||
analyze(this, context)
|
||||
}
|
||||
}
|
||||
@ -167,7 +156,8 @@ open class ClassAndMemberVisitor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Run action with a guard that populates [messages] based on the output.
|
||||
* Run action with a guard that populates [AnalysisRuntimeContext.messages]
|
||||
* based on the output.
|
||||
*/
|
||||
private inline fun captureExceptions(action: () -> Unit): Boolean {
|
||||
return try {
|
||||
@ -229,9 +219,7 @@ open class ClassAndMemberVisitor(
|
||||
ClassRepresentation(version, access, name, superClassName, interfaceNames, genericsDetails = signature ?: "").also {
|
||||
currentClass = it
|
||||
currentMember = null
|
||||
sourceLocation = SourceLocation(
|
||||
className = name
|
||||
)
|
||||
sourceLocation = SourceLocation(className = name)
|
||||
}
|
||||
captureExceptions {
|
||||
currentClass = visitClass(currentClass!!)
|
||||
@ -251,7 +239,7 @@ open class ClassAndMemberVisitor(
|
||||
override fun visitEnd() {
|
||||
configuration.classModule
|
||||
.getClassReferencesFromClass(currentClass!!, configuration.analyzeAnnotations)
|
||||
.forEach { recordTypeReference(it) }
|
||||
.forEach(::recordTypeReference)
|
||||
captureExceptions {
|
||||
visitClassEnd(currentClass!!)
|
||||
}
|
||||
@ -306,14 +294,15 @@ open class ClassAndMemberVisitor(
|
||||
configuration.memberModule.addToClass(clazz, visitedMember ?: member)
|
||||
return if (processMember) {
|
||||
val derivedMember = visitedMember ?: member
|
||||
val targetVisitor = super.visitMethod(
|
||||
derivedMember.access,
|
||||
derivedMember.memberName,
|
||||
derivedMember.signature,
|
||||
signature,
|
||||
derivedMember.exceptions.toTypedArray()
|
||||
)
|
||||
MethodVisitorImpl(targetVisitor)
|
||||
super.visitMethod(
|
||||
derivedMember.access,
|
||||
derivedMember.memberName,
|
||||
derivedMember.signature,
|
||||
signature,
|
||||
derivedMember.exceptions.toTypedArray()
|
||||
)?.let { targetVisitor ->
|
||||
MethodVisitorImpl(targetVisitor, derivedMember)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -340,14 +329,15 @@ open class ClassAndMemberVisitor(
|
||||
configuration.memberModule.addToClass(clazz, visitedMember ?: member)
|
||||
return if (processMember) {
|
||||
val derivedMember = visitedMember ?: member
|
||||
val targetVisitor = super.visitField(
|
||||
derivedMember.access,
|
||||
derivedMember.memberName,
|
||||
derivedMember.signature,
|
||||
signature,
|
||||
derivedMember.value
|
||||
)
|
||||
FieldVisitorImpl(targetVisitor)
|
||||
super.visitField(
|
||||
derivedMember.access,
|
||||
derivedMember.memberName,
|
||||
derivedMember.signature,
|
||||
signature,
|
||||
derivedMember.value
|
||||
)?.let { targetVisitor ->
|
||||
FieldVisitorImpl(targetVisitor)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -359,7 +349,8 @@ open class ClassAndMemberVisitor(
|
||||
* Visitor used to traverse and analyze a method.
|
||||
*/
|
||||
private inner class MethodVisitorImpl(
|
||||
targetVisitor: MethodVisitor?
|
||||
targetVisitor: MethodVisitor,
|
||||
private val method: Member
|
||||
) : MethodVisitor(API_VERSION, targetVisitor) {
|
||||
|
||||
/**
|
||||
@ -387,6 +378,16 @@ open class ClassAndMemberVisitor(
|
||||
return super.visitAnnotation(desc, visible)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write any new method body code, assuming the definition providers
|
||||
* have provided any. This handler will not be visited if this method
|
||||
* has no existing code.
|
||||
*/
|
||||
override fun visitCode() {
|
||||
tryReplaceMethodBody()
|
||||
super.visitCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract information about provided field access instruction.
|
||||
*/
|
||||
@ -493,6 +494,29 @@ open class ClassAndMemberVisitor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish visiting this method, writing any new method body byte-code
|
||||
* if we haven't written it already. This would (presumably) only happen
|
||||
* for methods that previously had no body, e.g. native methods.
|
||||
*/
|
||||
override fun visitEnd() {
|
||||
tryReplaceMethodBody()
|
||||
super.visitEnd()
|
||||
}
|
||||
|
||||
private fun tryReplaceMethodBody() {
|
||||
if (method.body.isNotEmpty() && (mv != null)) {
|
||||
EmitterModule(mv).apply {
|
||||
for (body in method.body) {
|
||||
body(this)
|
||||
}
|
||||
}
|
||||
mv.visitMaxs(-1, -1)
|
||||
mv.visitEnd()
|
||||
mv = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to streamline the access to an instruction and to catch any related processing errors.
|
||||
*/
|
||||
@ -517,7 +541,7 @@ open class ClassAndMemberVisitor(
|
||||
* Visitor used to traverse and analyze a field.
|
||||
*/
|
||||
private inner class FieldVisitorImpl(
|
||||
targetVisitor: FieldVisitor?
|
||||
targetVisitor: FieldVisitor
|
||||
) : FieldVisitor(API_VERSION, targetVisitor) {
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,8 @@
|
||||
package net.corda.djvm.analysis
|
||||
|
||||
import net.corda.djvm.code.asPackagePath
|
||||
import net.corda.djvm.code.asResourcePath
|
||||
|
||||
/**
|
||||
* Functionality for resolving the class name of a sandboxable class.
|
||||
*
|
||||
@ -32,12 +35,12 @@ class ClassResolver(
|
||||
*/
|
||||
fun resolve(name: String): String {
|
||||
return when {
|
||||
name.startsWith("[") && name.endsWith(";") -> {
|
||||
name.startsWith('[') && name.endsWith(';') -> {
|
||||
complexArrayTypeRegex.replace(name) {
|
||||
"${it.groupValues[1]}L${resolveName(it.groupValues[2])};"
|
||||
}
|
||||
}
|
||||
name.startsWith("[") && !name.endsWith(";") -> name
|
||||
name.startsWith('[') && !name.endsWith(';') -> name
|
||||
else -> resolveName(name)
|
||||
}
|
||||
}
|
||||
@ -46,7 +49,7 @@ class ClassResolver(
|
||||
* Resolve the class name from a fully qualified normalized name.
|
||||
*/
|
||||
fun resolveNormalized(name: String): String {
|
||||
return resolve(name.replace('.', '/')).replace('/', '.')
|
||||
return resolve(name.asResourcePath).asPackagePath
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,7 +99,7 @@ class ClassResolver(
|
||||
* Reverse the resolution of a class name from a fully qualified normalized name.
|
||||
*/
|
||||
fun reverseNormalized(name: String): String {
|
||||
return reverse(name.replace('.', '/')).replace('/', '.')
|
||||
return reverse(name.asResourcePath).asPackagePath
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,7 +117,8 @@ open class Whitelist private constructor(
|
||||
"^java/lang/Throwable(\\..*)?$".toRegex(),
|
||||
"^java/lang/Void(\\..*)?$".toRegex(),
|
||||
"^java/lang/.*Error(\\..*)?$".toRegex(),
|
||||
"^java/lang/.*Exception(\\..*)?$".toRegex()
|
||||
"^java/lang/.*Exception(\\..*)?$".toRegex(),
|
||||
"^java/lang/reflect/Array(\\..*)?$".toRegex()
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,7 @@ class ClassMutator(
|
||||
private val configuration: AnalysisConfiguration,
|
||||
private val definitionProviders: List<DefinitionProvider> = emptyList(),
|
||||
private val emitters: List<Emitter> = emptyList()
|
||||
) : ClassAndMemberVisitor(classVisitor, configuration = configuration) {
|
||||
) : ClassAndMemberVisitor(configuration, classVisitor) {
|
||||
|
||||
/**
|
||||
* Tracks whether any modifications have been applied to any of the processed class(es) and pertinent members.
|
||||
@ -82,7 +82,8 @@ class ClassMutator(
|
||||
*/
|
||||
override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) {
|
||||
val context = EmitterContext(currentAnalysisContext(), configuration, emitter)
|
||||
Processor.processEntriesOfType<Emitter>(emitters, analysisContext.messages) {
|
||||
// We need to apply the tracing emitters before the non-tracing ones.
|
||||
Processor.processEntriesOfType<Emitter>(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) {
|
||||
it.emit(context, instruction)
|
||||
}
|
||||
if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) {
|
||||
|
@ -1,7 +1,9 @@
|
||||
package net.corda.djvm.code
|
||||
|
||||
import org.objectweb.asm.Label
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import org.objectweb.asm.Type
|
||||
import sandbox.net.corda.djvm.costing.RuntimeCostAccounter
|
||||
|
||||
/**
|
||||
@ -29,7 +31,7 @@ class EmitterModule(
|
||||
/**
|
||||
* Emit instruction for creating a new object of type [typeName].
|
||||
*/
|
||||
fun new(typeName: String, opcode: Int = Opcodes.NEW) {
|
||||
fun new(typeName: String, opcode: Int = NEW) {
|
||||
hasEmittedCustomCode = true
|
||||
methodVisitor.visitTypeInsn(opcode, typeName)
|
||||
}
|
||||
@ -38,7 +40,7 @@ class EmitterModule(
|
||||
* Emit instruction for creating a new object of type [T].
|
||||
*/
|
||||
inline fun <reified T> new() {
|
||||
new(T::class.java.name)
|
||||
new(Type.getInternalName(T::class.java))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,7 +64,7 @@ class EmitterModule(
|
||||
*/
|
||||
fun invokeStatic(owner: String, name: String, descriptor: String, isInterface: Boolean = false) {
|
||||
hasEmittedCustomCode = true
|
||||
methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, isInterface)
|
||||
methodVisitor.visitMethodInsn(INVOKESTATIC, owner, name, descriptor, isInterface)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,14 +72,14 @@ class EmitterModule(
|
||||
*/
|
||||
fun invokeSpecial(owner: String, name: String, descriptor: String, isInterface: Boolean = false) {
|
||||
hasEmittedCustomCode = true
|
||||
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, descriptor, isInterface)
|
||||
methodVisitor.visitMethodInsn(INVOKESPECIAL, owner, name, descriptor, isInterface)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit instruction for invoking a special method on class [T], e.g. a constructor or a method on a super-type.
|
||||
*/
|
||||
inline fun <reified T> invokeSpecial(name: String, descriptor: String, isInterface: Boolean = false) {
|
||||
invokeSpecial(T::class.java.name, name, descriptor, isInterface)
|
||||
invokeSpecial(Type.getInternalName(T::class.java), name, descriptor, isInterface)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,7 +87,7 @@ class EmitterModule(
|
||||
*/
|
||||
fun pop() {
|
||||
hasEmittedCustomCode = true
|
||||
methodVisitor.visitInsn(Opcodes.POP)
|
||||
methodVisitor.visitInsn(POP)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,19 +95,40 @@ class EmitterModule(
|
||||
*/
|
||||
fun duplicate() {
|
||||
hasEmittedCustomCode = true
|
||||
methodVisitor.visitInsn(Opcodes.DUP)
|
||||
methodVisitor.visitInsn(DUP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a sequence of instructions for instantiating and throwing an exception based on the provided message.
|
||||
*/
|
||||
fun throwError(message: String) {
|
||||
fun <T : Throwable> throwException(exceptionType: Class<T>, message: String) {
|
||||
hasEmittedCustomCode = true
|
||||
new<java.lang.Exception>()
|
||||
methodVisitor.visitInsn(Opcodes.DUP)
|
||||
val exceptionName = Type.getInternalName(exceptionType)
|
||||
new(exceptionName)
|
||||
methodVisitor.visitInsn(DUP)
|
||||
methodVisitor.visitLdcInsn(message)
|
||||
invokeSpecial<java.lang.Exception>("<init>", "(Ljava/lang/String;)V")
|
||||
methodVisitor.visitInsn(Opcodes.ATHROW)
|
||||
invokeSpecial(exceptionName, "<init>", "(Ljava/lang/String;)V")
|
||||
methodVisitor.visitInsn(ATHROW)
|
||||
}
|
||||
|
||||
inline fun <reified T : Throwable> throwException(message: String) = throwException(T::class.java, message)
|
||||
|
||||
/**
|
||||
* Emit instruction for returning from "void" method.
|
||||
*/
|
||||
fun returnVoid() {
|
||||
methodVisitor.visitInsn(RETURN)
|
||||
hasEmittedCustomCode = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit instructions for a new line number.
|
||||
*/
|
||||
fun lineNumber(line: Int) {
|
||||
val label = Label()
|
||||
methodVisitor.visitLabel(label)
|
||||
methodVisitor.visitLineNumber(line, label)
|
||||
hasEmittedCustomCode = true
|
||||
}
|
||||
|
||||
/**
|
||||
|
15
djvm/src/main/kotlin/net/corda/djvm/code/Types.kt
Normal file
15
djvm/src/main/kotlin/net/corda/djvm/code/Types.kt
Normal file
@ -0,0 +1,15 @@
|
||||
@file:JvmName("Types")
|
||||
package net.corda.djvm.code
|
||||
|
||||
import org.objectweb.asm.Type
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
|
||||
val ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java)
|
||||
val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::class.java)
|
||||
|
||||
/**
|
||||
* Local extension method for normalizing a class name.
|
||||
*/
|
||||
val String.asPackagePath: String get() = this.replace('/', '.')
|
||||
val String.asResourcePath: String get() = this.replace('.', '/')
|
@ -1,6 +1,7 @@
|
||||
package net.corda.djvm.costing
|
||||
|
||||
import net.corda.djvm.utilities.loggerFor
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
|
||||
/**
|
||||
* Cost metric to be used in a sandbox environment. The metric has a threshold and a mechanism for reporting violations.
|
||||
@ -41,7 +42,7 @@ open class TypedRuntimeCost<T>(
|
||||
if (thresholdPredicate(newValue)) {
|
||||
val message = errorMessage(currentThread)
|
||||
logger.error("Threshold breached; {}", message)
|
||||
throw ThresholdViolationException(message)
|
||||
throw ThresholdViolationError(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package net.corda.djvm.execution
|
||||
|
||||
import net.corda.djvm.SandboxConfiguration
|
||||
import net.corda.djvm.source.ClassSource
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
* The executor is responsible for spinning up a deterministic, sandboxed environment and launching the referenced code
|
||||
@ -12,14 +13,14 @@ import net.corda.djvm.source.ClassSource
|
||||
* @param configuration The configuration of the sandbox.
|
||||
*/
|
||||
class DeterministicSandboxExecutor<TInput, TOutput>(
|
||||
configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT
|
||||
configuration: SandboxConfiguration
|
||||
) : SandboxExecutor<TInput, TOutput>(configuration) {
|
||||
|
||||
/**
|
||||
* Short-hand for running a [SandboxedRunnable] in a sandbox by its type reference.
|
||||
* Short-hand for running a [Function] in a sandbox by its type reference.
|
||||
*/
|
||||
inline fun <reified TRunnable : SandboxedRunnable<TInput, TOutput>> run(input: TInput):
|
||||
ExecutionSummaryWithResult<TOutput?> {
|
||||
inline fun <reified TRunnable : Function<in TInput, out TOutput>> run(input: TInput):
|
||||
ExecutionSummaryWithResult<TOutput> {
|
||||
return run(ClassSource.fromClassName(TRunnable::class.java.name), input)
|
||||
}
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
package net.corda.djvm.execution
|
||||
|
||||
/**
|
||||
* Functionality runnable by a sandbox executor, marked for discoverability.
|
||||
*/
|
||||
interface DiscoverableRunnable
|
@ -1,7 +1,7 @@
|
||||
package net.corda.djvm.execution
|
||||
|
||||
/**
|
||||
* The execution profile of a [SandboxedRunnable] when run in a sandbox.
|
||||
* The execution profile of a [java.util.function.Function] when run in a sandbox.
|
||||
*
|
||||
* @property allocationCostThreshold The threshold placed on allocations.
|
||||
* @property invocationCostThreshold The threshold placed on invocations.
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.djvm.execution
|
||||
|
||||
/**
|
||||
* The summary of the execution of a [SandboxedRunnable] in a sandbox. This class has no representation of the outcome,
|
||||
* and is typically used when there has been a pre-mature exit from the sandbox, for instance, if an exception was
|
||||
* thrown.
|
||||
* The summary of the execution of a [java.util.function.Function] in a sandbox. This class has no representation of the
|
||||
* outcome, and is typically used when there has been a pre-mature exit from the sandbox, for instance, if an exception
|
||||
* was thrown.
|
||||
*
|
||||
* @property costs The costs accumulated when running the sandboxed code.
|
||||
*/
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.djvm.execution
|
||||
|
||||
/**
|
||||
* The summary of the execution of a [SandboxedRunnable] in a sandbox.
|
||||
* The summary of the execution of a [java.util.function.Function] in a sandbox.
|
||||
*
|
||||
* @property result The outcome of the sandboxed operation.
|
||||
* @see ExecutionSummary
|
||||
|
@ -2,7 +2,6 @@ package net.corda.djvm.execution
|
||||
|
||||
import net.corda.djvm.SandboxConfiguration
|
||||
import net.corda.djvm.SandboxRuntimeContext
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.messages.MessageCollection
|
||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||
import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||
@ -16,8 +15,7 @@ import kotlin.concurrent.thread
|
||||
*/
|
||||
class IsolatedTask(
|
||||
private val identifier: String,
|
||||
private val configuration: SandboxConfiguration,
|
||||
private val context: AnalysisContext
|
||||
private val configuration: SandboxConfiguration
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -32,12 +30,12 @@ class IsolatedTask(
|
||||
var exception: Throwable? = null
|
||||
thread(name = threadName, isDaemon = true) {
|
||||
logger.trace("Entering isolated runtime environment...")
|
||||
SandboxRuntimeContext(configuration, context.inputClasses).use {
|
||||
SandboxRuntimeContext(configuration).use {
|
||||
output = try {
|
||||
action(runnable)
|
||||
} catch (ex: Throwable) {
|
||||
logger.error("Exception caught in isolated runtime environment", ex)
|
||||
exception = ex
|
||||
exception = (ex as? LinkageError)?.cause ?: ex
|
||||
null
|
||||
}
|
||||
costs = CostSummary(
|
||||
@ -84,7 +82,7 @@ class IsolatedTask(
|
||||
)
|
||||
|
||||
/**
|
||||
* The class loader to use for loading the [SandboxedRunnable] and any referenced code in [SandboxExecutor.run].
|
||||
* The class loader to use for loading the [java.util.function.Function] and any referenced code in [SandboxExecutor.run].
|
||||
*/
|
||||
val classLoader: SandboxClassLoader
|
||||
get() = SandboxRuntimeContext.instance.classLoader
|
||||
|
@ -11,7 +11,6 @@ import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||
import net.corda.djvm.source.ClassSource
|
||||
import net.corda.djvm.utilities.loggerFor
|
||||
import net.corda.djvm.validation.ReferenceValidationSummary
|
||||
import net.corda.djvm.validation.ReferenceValidator
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
|
||||
/**
|
||||
@ -22,7 +21,7 @@ import java.lang.reflect.InvocationTargetException
|
||||
* @property configuration The configuration of sandbox.
|
||||
*/
|
||||
open class SandboxExecutor<in TInput, out TOutput>(
|
||||
protected val configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT
|
||||
protected val configuration: SandboxConfiguration
|
||||
) {
|
||||
|
||||
private val classModule = configuration.analysisConfiguration.classModule
|
||||
@ -32,12 +31,7 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
private val whitelist = configuration.analysisConfiguration.whitelist
|
||||
|
||||
/**
|
||||
* Module used to validate all traversable references before instantiating and executing a [SandboxedRunnable].
|
||||
*/
|
||||
private val referenceValidator = ReferenceValidator(configuration.analysisConfiguration)
|
||||
|
||||
/**
|
||||
* Executes a [SandboxedRunnable] implementation.
|
||||
* Executes a [java.util.function.Function] implementation.
|
||||
*
|
||||
* @param runnableClass The entry point of the sandboxed code to run.
|
||||
* @param input The input to provide to the sandboxed environment.
|
||||
@ -50,7 +44,7 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
open fun run(
|
||||
runnableClass: ClassSource,
|
||||
input: TInput
|
||||
): ExecutionSummaryWithResult<TOutput?> {
|
||||
): ExecutionSummaryWithResult<TOutput> {
|
||||
// 1. We first do a breath first traversal of the class hierarchy, starting from the requested class.
|
||||
// The branching is defined by class references from referencesFromLocation.
|
||||
// 2. For each class we run validation against defined rules.
|
||||
@ -63,22 +57,22 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
// 6. For execution, we then load the top-level class, implementing the SandboxedRunnable interface, again and
|
||||
// and consequently hit the cache. Once loaded, we can execute the code on the spawned thread, i.e., in an
|
||||
// isolated environment.
|
||||
logger.trace("Executing {} with input {}...", runnableClass, input)
|
||||
logger.debug("Executing {} with input {}...", runnableClass, input)
|
||||
// TODO Class sources can be analyzed in parallel, although this require making the analysis context thread-safe
|
||||
// To do so, one could start by batching the first X classes from the class sources and analyse each one in
|
||||
// parallel, caching any intermediate state and subsequently process enqueued sources in parallel batches as well.
|
||||
// Note that this would require some rework of the [IsolatedTask] and the class loader to bypass the limitation
|
||||
// of caching and state preserved in thread-local contexts.
|
||||
val classSources = listOf(runnableClass)
|
||||
val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, classSources)
|
||||
val result = IsolatedTask(runnableClass.qualifiedClassName, configuration, context).run {
|
||||
val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration)
|
||||
val result = IsolatedTask(runnableClass.qualifiedClassName, configuration).run {
|
||||
validate(context, classLoader, classSources)
|
||||
val loadedClass = classLoader.loadClassAndBytes(runnableClass, context)
|
||||
val instance = loadedClass.type.newInstance()
|
||||
val method = loadedClass.type.getMethod("run", Any::class.java)
|
||||
val method = loadedClass.type.getMethod("apply", Any::class.java)
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
method.invoke(instance, input) as? TOutput?
|
||||
method.invoke(instance, input) as? TOutput
|
||||
} catch (ex: InvocationTargetException) {
|
||||
throw ex.targetException
|
||||
}
|
||||
@ -105,8 +99,8 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
* @return A [LoadedClass] with the class' byte code, type and name.
|
||||
*/
|
||||
fun load(classSource: ClassSource): LoadedClass {
|
||||
val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, listOf(classSource))
|
||||
val result = IsolatedTask("LoadClass", configuration, context).run {
|
||||
val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration)
|
||||
val result = IsolatedTask("LoadClass", configuration).run {
|
||||
classLoader.loadClassAndBytes(classSource, context)
|
||||
}
|
||||
return result.output ?: throw ClassNotFoundException(classSource.qualifiedClassName)
|
||||
@ -125,8 +119,8 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
@Throws(SandboxClassLoadingException::class)
|
||||
fun validate(vararg classSources: ClassSource): ReferenceValidationSummary {
|
||||
logger.trace("Validating {}...", classSources)
|
||||
val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, classSources.toList())
|
||||
val result = IsolatedTask("Validation", configuration, context).run {
|
||||
val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration)
|
||||
val result = IsolatedTask("Validation", configuration).run {
|
||||
validate(context, classLoader, classSources.toList())
|
||||
}
|
||||
logger.trace("Validation of {} resulted in {}", classSources, result)
|
||||
@ -172,10 +166,6 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
}
|
||||
failOnReportedErrorsInContext(context)
|
||||
|
||||
// Validate all references in class hierarchy before proceeding.
|
||||
referenceValidator.validate(context, classLoader.analyzer)
|
||||
failOnReportedErrorsInContext(context)
|
||||
|
||||
return ReferenceValidationSummary(context.classes, context.messages, context.classOrigins)
|
||||
}
|
||||
|
||||
@ -185,7 +175,7 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
private inline fun processClassQueue(
|
||||
vararg elements: ClassSource, action: QueueProcessor<ClassSource>.(ClassSource, String) -> Unit
|
||||
) {
|
||||
QueueProcessor({ it.qualifiedClassName }, *elements).process { classSource ->
|
||||
QueueProcessor(ClassSource::qualifiedClassName, *elements).process { classSource ->
|
||||
val className = classResolver.reverse(classModule.getBinaryClassName(classSource.qualifiedClassName))
|
||||
if (!whitelist.matches(className)) {
|
||||
action(classSource, className)
|
||||
|
@ -1,19 +0,0 @@
|
||||
package net.corda.djvm.execution
|
||||
|
||||
/**
|
||||
* Functionality runnable by a sandbox executor.
|
||||
*/
|
||||
interface SandboxedRunnable<in TInput, out TOutput> : DiscoverableRunnable {
|
||||
|
||||
/**
|
||||
* The entry point of the sandboxed functionality to be run.
|
||||
*
|
||||
* @param input The input to pass in to the entry point.
|
||||
*
|
||||
* @returns The output to pass back to the caller after the sandboxed code has finished running.
|
||||
* @throws Exception The function can throw an exception, in which case the exception gets passed to the caller.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun run(input: TInput): TOutput?
|
||||
|
||||
}
|
@ -53,7 +53,7 @@ class MemberFormatter(
|
||||
* Check whether or not a signature is for a method.
|
||||
*/
|
||||
fun isMethod(abbreviatedSignature: String): Boolean {
|
||||
return abbreviatedSignature.startsWith("(")
|
||||
return abbreviatedSignature.startsWith('(')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,8 +82,8 @@ class ClassHierarchy(
|
||||
return findAncestors(get(className)).plus(get(OBJECT_NAME))
|
||||
.asSequence()
|
||||
.filterNotNull()
|
||||
.map { memberModule.getFromClass(it, memberName, signature) }
|
||||
.firstOrNull { it != null }
|
||||
.mapNotNull { memberModule.getFromClass(it, memberName, signature) }
|
||||
.firstOrNull()
|
||||
.apply {
|
||||
logger.trace("Getting rooted member for {}.{}:{} yields {}", className, memberName, signature, this)
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package net.corda.djvm.references
|
||||
|
||||
import net.corda.djvm.code.asPackagePath
|
||||
import net.corda.djvm.code.asResourcePath
|
||||
|
||||
/**
|
||||
* Class-specific functionality.
|
||||
*/
|
||||
@ -42,14 +45,12 @@ class ClassModule : AnnotationModule() {
|
||||
/**
|
||||
* Get the binary version of a class name.
|
||||
*/
|
||||
fun getBinaryClassName(name: String) =
|
||||
normalizeClassName(name).replace('.', '/')
|
||||
fun getBinaryClassName(name: String) = normalizeClassName(name).asResourcePath
|
||||
|
||||
/**
|
||||
* Get the formatted version of a class name.
|
||||
*/
|
||||
fun getFormattedClassName(name: String) =
|
||||
normalizeClassName(name).replace('/', '.')
|
||||
fun getFormattedClassName(name: String) = normalizeClassName(name).asPackagePath
|
||||
|
||||
/**
|
||||
* Get the short name of a class.
|
||||
|
@ -1,5 +1,13 @@
|
||||
package net.corda.djvm.references
|
||||
|
||||
import net.corda.djvm.code.EmitterModule
|
||||
|
||||
/**
|
||||
* Alias for a handler which will replace an entire
|
||||
* method body with a block of byte-code.
|
||||
*/
|
||||
typealias MethodBody = (EmitterModule) -> Unit
|
||||
|
||||
/**
|
||||
* Representation of a class member.
|
||||
*
|
||||
@ -11,6 +19,7 @@ package net.corda.djvm.references
|
||||
* @property annotations The names of the annotations the member is attributed.
|
||||
* @property exceptions The names of the exceptions that the member can throw.
|
||||
* @property value The default value of a field.
|
||||
* @property body One or more handlers to replace the method body with new byte-code.
|
||||
*/
|
||||
data class Member(
|
||||
override val access: Int,
|
||||
@ -20,5 +29,6 @@ data class Member(
|
||||
val genericsDetails: String,
|
||||
val annotations: MutableSet<String> = mutableSetOf(),
|
||||
val exceptions: MutableSet<String> = mutableSetOf(),
|
||||
val value: Any? = null
|
||||
val value: Any? = null,
|
||||
val body: List<MethodBody> = emptyList()
|
||||
) : MemberInformation, EntityWithAccessFlag
|
||||
|
@ -33,14 +33,14 @@ class MemberModule : AnnotationModule() {
|
||||
* Check if member is a field.
|
||||
*/
|
||||
fun isField(member: MemberInformation): Boolean {
|
||||
return !member.signature.startsWith("(")
|
||||
return !member.signature.startsWith('(')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if member is a method.
|
||||
*/
|
||||
fun isMethod(member: MemberInformation): Boolean {
|
||||
return member.signature.startsWith("(")
|
||||
return member.signature.startsWith('(')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,11 @@ class ReferenceMap(
|
||||
|
||||
private val referencesPerLocation: MutableMap<String, MutableSet<ReferenceWithLocation>> = hashMapOf()
|
||||
|
||||
private var numberOfReferences = 0
|
||||
/**
|
||||
* The number of references in the map.
|
||||
*/
|
||||
var numberOfReferences = 0
|
||||
private set
|
||||
|
||||
/**
|
||||
* Add source location association to a target member.
|
||||
@ -50,12 +54,6 @@ class ReferenceMap(
|
||||
return referencesPerLocation.getOrElse(key(className, memberName, signature)) { emptySet() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of member references in the map.
|
||||
*/
|
||||
val size: Int
|
||||
get() = numberOfReferences
|
||||
|
||||
/**
|
||||
* Get iterator for all the references in the map.
|
||||
*/
|
||||
|
@ -27,7 +27,7 @@ open class ClassRewriter(
|
||||
* @param context The context in which the class is being analyzed and processed.
|
||||
*/
|
||||
fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode {
|
||||
logger.trace("Rewriting class {}...", reader.className)
|
||||
logger.debug("Rewriting class {}...", reader.className)
|
||||
val writer = SandboxClassWriter(reader, classLoader)
|
||||
val classRemapper = ClassRemapper(writer, remapper)
|
||||
val visitor = ClassMutator(
|
||||
|
@ -3,29 +3,31 @@ package net.corda.djvm.rewiring
|
||||
import net.corda.djvm.SandboxConfiguration
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.analysis.ClassAndMemberVisitor
|
||||
import net.corda.djvm.code.asResourcePath
|
||||
import net.corda.djvm.references.ClassReference
|
||||
import net.corda.djvm.source.ClassSource
|
||||
import net.corda.djvm.source.SourceClassLoader
|
||||
import net.corda.djvm.utilities.loggerFor
|
||||
import net.corda.djvm.validation.RuleValidator
|
||||
|
||||
/**
|
||||
* Class loader that enables registration of rewired classes.
|
||||
*
|
||||
* @property configuration The configuration to use for the sandbox.
|
||||
* @param configuration The configuration to use for the sandbox.
|
||||
* @property context The context in which analysis and processing is performed.
|
||||
*/
|
||||
class SandboxClassLoader(
|
||||
val configuration: SandboxConfiguration,
|
||||
val context: AnalysisContext
|
||||
) : ClassLoader() {
|
||||
configuration: SandboxConfiguration,
|
||||
private val context: AnalysisContext
|
||||
) : ClassLoader(null) {
|
||||
|
||||
private val analysisConfiguration = configuration.analysisConfiguration
|
||||
|
||||
/**
|
||||
* The instance used to validate that any loaded class complies with the specified rules.
|
||||
*/
|
||||
private val ruleValidator: RuleValidator = RuleValidator(
|
||||
rules = configuration.rules,
|
||||
configuration = configuration.analysisConfiguration
|
||||
configuration = analysisConfiguration
|
||||
)
|
||||
|
||||
/**
|
||||
@ -37,12 +39,12 @@ class SandboxClassLoader(
|
||||
/**
|
||||
* Set of classes that should be left untouched due to pinning.
|
||||
*/
|
||||
private val pinnedClasses = configuration.analysisConfiguration.pinnedClasses
|
||||
private val pinnedClasses = analysisConfiguration.pinnedClasses
|
||||
|
||||
/**
|
||||
* Set of classes that should be left untouched due to whitelisting.
|
||||
*/
|
||||
private val whitelistedClasses = configuration.analysisConfiguration.whitelist
|
||||
private val whitelistedClasses = analysisConfiguration.whitelist
|
||||
|
||||
/**
|
||||
* Cache of loaded classes.
|
||||
@ -52,10 +54,7 @@ class SandboxClassLoader(
|
||||
/**
|
||||
* The class loader used to find classes on the extended class path.
|
||||
*/
|
||||
private val supportingClassLoader = SourceClassLoader(
|
||||
configuration.analysisConfiguration.classPath,
|
||||
configuration.analysisConfiguration.classResolver
|
||||
)
|
||||
private val supportingClassLoader = analysisConfiguration.supportingClassLoader
|
||||
|
||||
/**
|
||||
* The re-writer to use for registered classes.
|
||||
@ -83,9 +82,9 @@ class SandboxClassLoader(
|
||||
* @return The resulting <tt>Class</tt> object and its byte code representation.
|
||||
*/
|
||||
fun loadClassAndBytes(source: ClassSource, context: AnalysisContext): LoadedClass {
|
||||
logger.trace("Loading class {}, origin={}...", source.qualifiedClassName, source.origin)
|
||||
val name = configuration.analysisConfiguration.classResolver.reverseNormalized(source.qualifiedClassName)
|
||||
val resolvedName = configuration.analysisConfiguration.classResolver.resolveNormalized(name)
|
||||
logger.debug("Loading class {}, origin={}...", source.qualifiedClassName, source.origin)
|
||||
val name = analysisConfiguration.classResolver.reverseNormalized(source.qualifiedClassName)
|
||||
val resolvedName = analysisConfiguration.classResolver.resolveNormalized(name)
|
||||
|
||||
// Check if the class has already been loaded.
|
||||
val loadedClass = loadedClasses[name]
|
||||
@ -99,14 +98,14 @@ class SandboxClassLoader(
|
||||
|
||||
// Analyse the class if not matching the whitelist.
|
||||
val readClassName = reader.className
|
||||
if (!configuration.analysisConfiguration.whitelist.matches(readClassName)) {
|
||||
if (!analysisConfiguration.whitelist.matches(readClassName)) {
|
||||
logger.trace("Class {} does not match with the whitelist", source.qualifiedClassName)
|
||||
logger.trace("Analyzing class {}...", source.qualifiedClassName)
|
||||
analyzer.analyze(reader, context)
|
||||
}
|
||||
|
||||
// Check if the class should be left untouched.
|
||||
val qualifiedName = name.replace('.', '/')
|
||||
val qualifiedName = name.asResourcePath
|
||||
if (qualifiedName in pinnedClasses) {
|
||||
logger.trace("Class {} is marked as pinned", source.qualifiedClassName)
|
||||
val pinnedClasses = LoadedClass(
|
||||
@ -146,7 +145,7 @@ class SandboxClassLoader(
|
||||
context.recordClassOrigin(name, ClassReference(source.origin))
|
||||
}
|
||||
|
||||
logger.trace("Loaded class {}, bytes={}, isModified={}",
|
||||
logger.debug("Loaded class {}, bytes={}, isModified={}",
|
||||
source.qualifiedClassName, byteCode.bytes.size, byteCode.isModified)
|
||||
|
||||
return classWithByteCode
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.djvm.rewiring
|
||||
|
||||
import net.corda.djvm.code.asPackagePath
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES
|
||||
@ -21,26 +22,28 @@ import org.objectweb.asm.Type
|
||||
*/
|
||||
open class SandboxClassWriter(
|
||||
classReader: ClassReader,
|
||||
private val classLoader: ClassLoader,
|
||||
private val cloader: ClassLoader,
|
||||
flags: Int = COMPUTE_FRAMES or COMPUTE_MAXS
|
||||
) : ClassWriter(classReader, flags) {
|
||||
|
||||
override fun getClassLoader(): ClassLoader = cloader
|
||||
|
||||
/**
|
||||
* Get the common super type of [type1] and [type2].
|
||||
*/
|
||||
override fun getCommonSuperClass(type1: String, type2: String): String {
|
||||
// Need to override [getCommonSuperClass] to ensure that the correct class loader is used.
|
||||
// Need to override [getCommonSuperClass] to ensure that we use ClassLoader.loadClass().
|
||||
when {
|
||||
type1 == OBJECT_NAME -> return type1
|
||||
type2 == OBJECT_NAME -> return type2
|
||||
}
|
||||
val class1 = try {
|
||||
classLoader.loadClass(type1.replace('/', '.'))
|
||||
classLoader.loadClass(type1.asPackagePath)
|
||||
} catch (exception: Exception) {
|
||||
throw TypeNotPresentException(type1, exception)
|
||||
}
|
||||
val class2 = try {
|
||||
classLoader.loadClass(type2.replace('/', '.'))
|
||||
classLoader.loadClass(type2.asPackagePath)
|
||||
} catch (exception: Exception) {
|
||||
throw TypeNotPresentException(type2, exception)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ abstract class ClassRule : Rule {
|
||||
*/
|
||||
abstract fun validate(context: RuleContext, clazz: ClassRepresentation)
|
||||
|
||||
override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) {
|
||||
final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) {
|
||||
// Only run validation step if applied to the class itself.
|
||||
if (clazz != null && member == null && instruction == null) {
|
||||
validate(context, clazz)
|
||||
|
@ -18,7 +18,7 @@ abstract class InstructionRule : Rule {
|
||||
*/
|
||||
abstract fun validate(context: RuleContext, instruction: Instruction)
|
||||
|
||||
override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) {
|
||||
final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) {
|
||||
// Only run validation step if applied to the class member itself.
|
||||
if (clazz != null && member != null && instruction != null) {
|
||||
validate(context, instruction)
|
||||
|
@ -18,7 +18,7 @@ abstract class MemberRule : Rule {
|
||||
*/
|
||||
abstract fun validate(context: RuleContext, member: Member)
|
||||
|
||||
override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) {
|
||||
final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) {
|
||||
// Only run validation step if applied to the class member itself.
|
||||
if (clazz != null && member != null && instruction == null) {
|
||||
validate(context, member)
|
||||
|
@ -1,17 +0,0 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.Instruction.Companion.OP_BREAKPOINT
|
||||
import net.corda.djvm.rules.InstructionRule
|
||||
import net.corda.djvm.validation.RuleContext
|
||||
|
||||
/**
|
||||
* Rule that checks for invalid breakpoint instructions.
|
||||
*/
|
||||
class DisallowBreakpoints : InstructionRule() {
|
||||
|
||||
override fun validate(context: RuleContext, instruction: Instruction) = context.validate {
|
||||
fail("Disallowed breakpoint in method") given (instruction.operation == OP_BREAKPOINT)
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +1,16 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.*
|
||||
import net.corda.djvm.code.instructions.CodeLabel
|
||||
import net.corda.djvm.code.instructions.TryCatchBlock
|
||||
import net.corda.djvm.costing.ThresholdViolationException
|
||||
import net.corda.djvm.rules.InstructionRule
|
||||
import net.corda.djvm.validation.RuleContext
|
||||
import org.objectweb.asm.Label
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
|
||||
/**
|
||||
* Rule that checks for attempted catches of [ThreadDeath], [ThresholdViolationException], [StackOverflowError],
|
||||
* [OutOfMemoryError], [Error] or [Throwable].
|
||||
* Rule that checks for attempted catches of [ThreadDeath], [ThresholdViolationError],
|
||||
* [StackOverflowError], [OutOfMemoryError], [Error] or [Throwable].
|
||||
*/
|
||||
class DisallowCatchingBlacklistedExceptions : InstructionRule(), Emitter {
|
||||
|
||||
override fun validate(context: RuleContext, instruction: Instruction) = context.validate {
|
||||
if (instruction is TryCatchBlock) {
|
||||
val typeName = context.classModule.getFormattedClassName(instruction.typeName)
|
||||
warn("Injected runtime check for catch-block for type $typeName") given
|
||||
(instruction.typeName in disallowedExceptionTypes)
|
||||
fail("Disallowed catch of ThreadDeath exception") given
|
||||
(instruction.typeName == threadDeathException)
|
||||
fail("Disallowed catch of stack overflow exception") given
|
||||
(instruction.typeName == stackOverflowException)
|
||||
fail("Disallowed catch of out of memory exception") given
|
||||
(instruction.typeName == outOfMemoryException)
|
||||
fail("Disallowed catch of threshold violation exception") given
|
||||
(instruction.typeName.endsWith(ThresholdViolationException::class.java.simpleName))
|
||||
}
|
||||
}
|
||||
class DisallowCatchingBlacklistedExceptions : Emitter {
|
||||
|
||||
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||
if (instruction is TryCatchBlock && instruction.typeName in disallowedExceptionTypes) {
|
||||
@ -46,13 +26,27 @@ class DisallowCatchingBlacklistedExceptions : InstructionRule(), Emitter {
|
||||
private fun isExceptionHandler(label: Label) = label in handlers
|
||||
|
||||
companion object {
|
||||
|
||||
private const val threadDeathException = "java/lang/ThreadDeath"
|
||||
private const val stackOverflowException = "java/lang/StackOverflowError"
|
||||
private const val outOfMemoryException = "java/lang/OutOfMemoryError"
|
||||
|
||||
// Any of [ThreadDeath]'s throwable super-classes need explicit checking.
|
||||
private val disallowedExceptionTypes = setOf(
|
||||
ruleViolationError,
|
||||
thresholdViolationError,
|
||||
|
||||
/**
|
||||
* These errors indicate that the JVM is failing,
|
||||
* so don't allow these to be caught either.
|
||||
*/
|
||||
"java/lang/StackOverflowError",
|
||||
"java/lang/OutOfMemoryError",
|
||||
|
||||
/**
|
||||
* These are immediate super-classes for our explicit errors.
|
||||
*/
|
||||
"java/lang/VirtualMachineError",
|
||||
"java/lang/ThreadDeath",
|
||||
|
||||
/**
|
||||
* Any of [ThreadDeath] and [VirtualMachineError]'s throwable
|
||||
* super-classes also need explicit checking.
|
||||
*/
|
||||
"java/lang/Throwable",
|
||||
"java/lang/Error"
|
||||
)
|
||||
|
@ -1,17 +0,0 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.references.Member
|
||||
import net.corda.djvm.rules.MemberRule
|
||||
import net.corda.djvm.validation.RuleContext
|
||||
|
||||
/**
|
||||
* Rule that checks for invalid use of finalizers.
|
||||
*/
|
||||
class DisallowFinalizerMethods : MemberRule() {
|
||||
|
||||
override fun validate(context: RuleContext, member: Member) = context.validate {
|
||||
fail("Disallowed finalizer method") given ("${member.memberName}:${member.signature}" == "finalize:()V")
|
||||
// TODO Make this rule simply erase the finalize() method and continue execution.
|
||||
}
|
||||
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.references.Member
|
||||
import net.corda.djvm.rules.MemberRule
|
||||
import net.corda.djvm.validation.RuleContext
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
/**
|
||||
* Rule that checks for invalid use of native methods.
|
||||
*/
|
||||
class DisallowNativeMethods : MemberRule() {
|
||||
|
||||
override fun validate(context: RuleContext, member: Member) = context.validate {
|
||||
fail("Disallowed native method") given Modifier.isNative(member.access)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.instructions.MemberAccessInstruction
|
||||
import net.corda.djvm.formatting.MemberFormatter
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
|
||||
/**
|
||||
* Some non-deterministic APIs belong to pinned classes and so cannot be stubbed out.
|
||||
* Replace their invocations with exceptions instead.
|
||||
*/
|
||||
class DisallowNonDeterministicMethods : Emitter {
|
||||
|
||||
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||
if (instruction is MemberAccessInstruction && isForbidden(instruction)) {
|
||||
when (instruction.operation) {
|
||||
INVOKEVIRTUAL -> {
|
||||
throwException<RuleViolationError>("Disallowed reference to API; ${memberFormatter.format(instruction.member)}")
|
||||
preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isClassReflection(instruction: MemberAccessInstruction): Boolean =
|
||||
(instruction.owner == "java/lang/Class") && (
|
||||
((instruction.memberName == "newInstance" && instruction.signature == "()Ljava/lang/Object;")
|
||||
|| instruction.signature.contains("Ljava/lang/reflect/"))
|
||||
)
|
||||
|
||||
private fun isObjectMonitor(instruction: MemberAccessInstruction): Boolean =
|
||||
(instruction.signature == "()V" && (instruction.memberName == "notify" || instruction.memberName == "notifyAll" || instruction.memberName == "wait"))
|
||||
|| (instruction.memberName == "wait" && (instruction.signature == "(J)V" || instruction.signature == "(JI)V"))
|
||||
|
||||
private fun isForbidden(instruction: MemberAccessInstruction): Boolean
|
||||
= instruction.isMethod && (isClassReflection(instruction) || isObjectMonitor(instruction))
|
||||
|
||||
private val memberFormatter = MemberFormatter()
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.instructions.MemberAccessInstruction
|
||||
import net.corda.djvm.formatting.MemberFormatter
|
||||
import net.corda.djvm.rules.InstructionRule
|
||||
import net.corda.djvm.validation.RuleContext
|
||||
|
||||
/**
|
||||
* Rule that checks for illegal references to reflection APIs.
|
||||
*/
|
||||
class DisallowReflection : InstructionRule() {
|
||||
|
||||
override fun validate(context: RuleContext, instruction: Instruction) = context.validate {
|
||||
// TODO Enable controlled use of reflection APIs
|
||||
if (instruction is MemberAccessInstruction) {
|
||||
invalidReflectionUsage(instruction) given
|
||||
("java/lang/Class" in instruction.owner && instruction.memberName == "newInstance")
|
||||
invalidReflectionUsage(instruction) given (instruction.owner.startsWith("java/lang/reflect/"))
|
||||
invalidReflectionUsage(instruction) given (instruction.owner.startsWith("java/lang/invoke/"))
|
||||
invalidReflectionUsage(instruction) given (instruction.owner.startsWith("sun/"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun RuleContext.invalidReflectionUsage(instruction: MemberAccessInstruction) =
|
||||
this.fail("Disallowed reference to reflection API; ${memberFormatter.format(instruction.member)}")
|
||||
|
||||
private val memberFormatter = MemberFormatter()
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.code.Instruction.Companion.OP_BREAKPOINT
|
||||
|
||||
/**
|
||||
* Rule that deletes invalid breakpoint instructions.
|
||||
*/
|
||||
class IgnoreBreakpoints : Emitter {
|
||||
|
||||
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||
when (instruction.operation) {
|
||||
OP_BREAKPOINT -> preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -3,20 +3,13 @@ package net.corda.djvm.rules.implementation
|
||||
import net.corda.djvm.code.Emitter
|
||||
import net.corda.djvm.code.EmitterContext
|
||||
import net.corda.djvm.code.Instruction
|
||||
import net.corda.djvm.rules.InstructionRule
|
||||
import net.corda.djvm.validation.RuleContext
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
|
||||
/**
|
||||
* Rule that warns about the use of synchronized code blocks. This class also exposes an emitter that rewrites pertinent
|
||||
* monitoring instructions to [POP]'s, as these replacements will remove the object references that [MONITORENTER] and
|
||||
* [MONITOREXIT] anticipate to be on the stack.
|
||||
* An emitter that rewrites monitoring instructions to [POP]s, as these replacements will remove
|
||||
* the object references that [MONITORENTER] and [MONITOREXIT] anticipate to be on the stack.
|
||||
*/
|
||||
class IgnoreSynchronizedBlocks : InstructionRule(), Emitter {
|
||||
|
||||
override fun validate(context: RuleContext, instruction: Instruction) = context.validate {
|
||||
inform("Stripped monitoring instruction") given (instruction.operation in setOf(MONITORENTER, MONITOREXIT))
|
||||
}
|
||||
class IgnoreSynchronizedBlocks : Emitter {
|
||||
|
||||
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||
when (instruction.operation) {
|
||||
|
@ -0,0 +1,35 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||
import net.corda.djvm.code.EmitterModule
|
||||
import net.corda.djvm.code.MemberDefinitionProvider
|
||||
import net.corda.djvm.references.Member
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
/**
|
||||
* Rule that replaces a finalize() method with a simple stub.
|
||||
*/
|
||||
class StubOutFinalizerMethods : MemberDefinitionProvider {
|
||||
|
||||
override fun define(context: AnalysisRuntimeContext, member: Member) = when {
|
||||
/**
|
||||
* Discard any other method body and replace with stub that just returns.
|
||||
* Other [MemberDefinitionProvider]s are expected to append to this list
|
||||
* and not replace its contents!
|
||||
*/
|
||||
isFinalizer(member) -> member.copy(body = listOf(::writeMethodBody))
|
||||
else -> member
|
||||
}
|
||||
|
||||
private fun writeMethodBody(emitter: EmitterModule): Unit = with(emitter) {
|
||||
returnVoid()
|
||||
}
|
||||
|
||||
/**
|
||||
* No need to rewrite [Object.finalize] or [Enum.finalize]; ignore these.
|
||||
*/
|
||||
private fun isFinalizer(member: Member): Boolean
|
||||
= member.memberName == "finalize" && member.signature == "()V"
|
||||
&& !member.className.startsWith("java/lang/")
|
||||
&& !Modifier.isAbstract(member.access)
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||
import net.corda.djvm.code.EmitterModule
|
||||
import net.corda.djvm.code.MemberDefinitionProvider
|
||||
import net.corda.djvm.references.Member
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
/**
|
||||
* Rule that replaces a native method with a stub that throws an exception.
|
||||
*/
|
||||
class StubOutNativeMethods : MemberDefinitionProvider {
|
||||
|
||||
override fun define(context: AnalysisRuntimeContext, member: Member) = when {
|
||||
isNative(member) -> member.copy(
|
||||
access = member.access and ACC_NATIVE.inv(),
|
||||
body = member.body + if (isForStubbing(member)) ::writeStubMethodBody else ::writeExceptionMethodBody
|
||||
)
|
||||
else -> member
|
||||
}
|
||||
|
||||
private fun writeExceptionMethodBody(emitter: EmitterModule): Unit = with(emitter) {
|
||||
lineNumber(0)
|
||||
throwException(RuleViolationError::class.java, "Native method has been deleted")
|
||||
}
|
||||
|
||||
private fun writeStubMethodBody(emitter: EmitterModule): Unit = with(emitter) {
|
||||
returnVoid()
|
||||
}
|
||||
|
||||
private fun isForStubbing(member: Member): Boolean = member.signature == "()V" && member.memberName == "registerNatives"
|
||||
|
||||
private fun isNative(member: Member): Boolean = Modifier.isNative(member.access)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package net.corda.djvm.rules.implementation
|
||||
|
||||
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||
import net.corda.djvm.code.EmitterModule
|
||||
import net.corda.djvm.code.MemberDefinitionProvider
|
||||
import net.corda.djvm.references.Member
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
|
||||
/**
|
||||
* Replace reflection APIs with stubs that throw exceptions. Only for unpinned classes.
|
||||
*/
|
||||
class StubOutReflectionMethods : MemberDefinitionProvider {
|
||||
|
||||
override fun define(context: AnalysisRuntimeContext, member: Member): Member = when {
|
||||
isConcreteApi(member) && isReflection(member) -> member.copy(body = member.body + ::writeMethodBody)
|
||||
else -> member
|
||||
}
|
||||
|
||||
private fun writeMethodBody(emitter: EmitterModule): Unit = with(emitter) {
|
||||
lineNumber(0)
|
||||
throwException(RuleViolationError::class.java, "Disallowed reference to reflection API")
|
||||
}
|
||||
|
||||
// The method must be public and with a Java implementation.
|
||||
private fun isConcreteApi(member: Member): Boolean = member.access and (ACC_PUBLIC or ACC_ABSTRACT or ACC_NATIVE) == ACC_PUBLIC
|
||||
|
||||
private fun isReflection(member: Member): Boolean {
|
||||
return member.className.startsWith("java/lang/reflect/")
|
||||
|| member.className.startsWith("java/lang/invoke/")
|
||||
|| member.className.startsWith("sun/reflect/")
|
||||
|| member.className == "sun/misc/Unsafe"
|
||||
|| member.className == "sun/misc/VM"
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package net.corda.djvm.source
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.analysis.ClassResolver
|
||||
import net.corda.djvm.analysis.SourceLocation
|
||||
import net.corda.djvm.code.asResourcePath
|
||||
import net.corda.djvm.messages.Message
|
||||
import net.corda.djvm.messages.Severity
|
||||
import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||
@ -17,18 +18,11 @@ import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.streams.toList
|
||||
|
||||
/**
|
||||
* Customizable class loader that allows the user to explicitly specify additional JARs and directories to scan.
|
||||
*
|
||||
* @param paths The directories and explicit JAR files to scan.
|
||||
* @property classResolver The resolver to use to derive the original name of a requested class.
|
||||
* @property resolvedUrls The resolved URLs that get passed to the underlying class loader.
|
||||
*/
|
||||
open class SourceClassLoader(
|
||||
paths: List<Path>,
|
||||
private val classResolver: ClassResolver,
|
||||
val resolvedUrls: Array<URL> = resolvePaths(paths)
|
||||
) : URLClassLoader(resolvedUrls, SourceClassLoader::class.java.classLoader) {
|
||||
abstract class AbstractSourceClassLoader(
|
||||
paths: List<Path>,
|
||||
private val classResolver: ClassResolver,
|
||||
parent: ClassLoader?
|
||||
) : URLClassLoader(resolvePaths(paths), parent) {
|
||||
|
||||
/**
|
||||
* Open a [ClassReader] for the provided class name.
|
||||
@ -36,7 +30,7 @@ open class SourceClassLoader(
|
||||
fun classReader(
|
||||
className: String, context: AnalysisContext, origin: String? = null
|
||||
): ClassReader {
|
||||
val originalName = classResolver.reverse(className.replace('.', '/'))
|
||||
val originalName = classResolver.reverse(className.asResourcePath)
|
||||
return try {
|
||||
logger.trace("Opening ClassReader for class {}...", originalName)
|
||||
getResourceAsStream("$originalName.class").use {
|
||||
@ -71,16 +65,16 @@ open class SourceClassLoader(
|
||||
return super.loadClass(originalName, resolve)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private val logger = loggerFor<SourceClassLoader>()
|
||||
protected companion object {
|
||||
@JvmStatic
|
||||
protected val logger = loggerFor<SourceClassLoader>()
|
||||
|
||||
private fun resolvePaths(paths: List<Path>): Array<URL> {
|
||||
return paths.map(this::expandPath).flatMap {
|
||||
when {
|
||||
!Files.exists(it) -> throw FileNotFoundException("File not found; $it")
|
||||
Files.isDirectory(it) -> {
|
||||
listOf(it.toURL()) + Files.list(it).filter(::isJarFile).map { it.toURL() }.toList()
|
||||
listOf(it.toURL()) + Files.list(it).filter(::isJarFile).map { jar -> jar.toURL() }.toList()
|
||||
}
|
||||
Files.isReadable(it) && isJarFile(it) -> listOf(it.toURL())
|
||||
else -> throw IllegalArgumentException("Expected JAR or class file, but found $it")
|
||||
@ -100,11 +94,76 @@ open class SourceClassLoader(
|
||||
|
||||
private fun isJarFile(path: Path) = path.toString().endsWith(".jar", true)
|
||||
|
||||
private fun Path.toURL() = this.toUri().toURL()
|
||||
private fun Path.toURL(): URL = this.toUri().toURL()
|
||||
|
||||
private val homeDirectory: Path
|
||||
get() = Paths.get(System.getProperty("user.home"))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Class loader to manage an optional JAR of replacement Java APIs.
|
||||
* @param bootstrapJar The location of the JAR containing the Java APIs.
|
||||
* @param classResolver The resolver to use to derive the original name of a requested class.
|
||||
*/
|
||||
class BootstrapClassLoader(
|
||||
bootstrapJar: Path,
|
||||
classResolver: ClassResolver
|
||||
) : AbstractSourceClassLoader(listOf(bootstrapJar), classResolver, null) {
|
||||
|
||||
/**
|
||||
* Only search our own jars for the given resource.
|
||||
*/
|
||||
override fun getResource(name: String): URL? = findResource(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizable class loader that allows the user to explicitly specify additional JARs and directories to scan.
|
||||
*
|
||||
* @param paths The directories and explicit JAR files to scan.
|
||||
* @property classResolver The resolver to use to derive the original name of a requested class.
|
||||
* @property bootstrap The [BootstrapClassLoader] containing the Java APIs for the sandbox.
|
||||
*/
|
||||
class SourceClassLoader(
|
||||
paths: List<Path>,
|
||||
classResolver: ClassResolver,
|
||||
private val bootstrap: BootstrapClassLoader? = null
|
||||
) : AbstractSourceClassLoader(paths, classResolver, SourceClassLoader::class.java.classLoader) {
|
||||
|
||||
/**
|
||||
* First check the bootstrap classloader, if we have one.
|
||||
* Otherwise check our parent classloader, followed by
|
||||
* the user-supplied jars.
|
||||
*/
|
||||
override fun getResource(name: String): URL? {
|
||||
if (bootstrap != null) {
|
||||
val resource = bootstrap.findResource(name)
|
||||
if (resource != null) {
|
||||
return resource
|
||||
} else if (isJvmInternal(name)) {
|
||||
logger.error("Denying request for actual {}", name)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return parent?.getResource(name) ?: findResource(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny all requests for DJVM classes from any user-supplied jars.
|
||||
*/
|
||||
override fun findResource(name: String): URL? {
|
||||
return if (name.startsWith("net/corda/djvm/")) null else super.findResource(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Does [name] exist within any of the packages reserved for Java itself?
|
||||
*/
|
||||
private fun isJvmInternal(name: String): Boolean = name.startsWith("java/")
|
||||
|| name.startsWith("javax/")
|
||||
|| name.startsWith("com/sun/")
|
||||
|| name.startsWith("sun/")
|
||||
|| name.startsWith("jdk/")
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
package net.corda.djvm.validation
|
||||
|
||||
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||
import net.corda.djvm.analysis.AnalysisContext
|
||||
import net.corda.djvm.analysis.ClassAndMemberVisitor
|
||||
import net.corda.djvm.execution.SandboxedRunnable
|
||||
import net.corda.djvm.formatting.MemberFormatter
|
||||
import net.corda.djvm.messages.Message
|
||||
import net.corda.djvm.messages.Severity
|
||||
import net.corda.djvm.references.*
|
||||
import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||
import net.corda.djvm.utilities.loggerFor
|
||||
|
||||
/**
|
||||
* Module used to validate all traversable references before instantiating and executing a [SandboxedRunnable].
|
||||
*
|
||||
* @param configuration The analysis configuration to use for the validation.
|
||||
* @property memberFormatter Module with functionality for formatting class members.
|
||||
*/
|
||||
class ReferenceValidator(
|
||||
private val configuration: AnalysisConfiguration,
|
||||
private val memberFormatter: MemberFormatter = MemberFormatter()
|
||||
) {
|
||||
|
||||
/**
|
||||
* Container holding the current state of the validation.
|
||||
*
|
||||
* @property context The context in which references are to be validated.
|
||||
* @property analyzer Underlying analyzer used for processing classes.
|
||||
*/
|
||||
private class State(
|
||||
val context: AnalysisContext,
|
||||
val analyzer: ClassAndMemberVisitor
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate whether or not the classes in a class hierarchy can be safely instantiated and run in a sandbox by
|
||||
* checking that all references are rooted in deterministic code.
|
||||
*
|
||||
* @param context The context in which the check should be made.
|
||||
* @param analyzer Underlying analyzer used for processing classes.
|
||||
*/
|
||||
fun validate(context: AnalysisContext, analyzer: ClassAndMemberVisitor): ReferenceValidationSummary =
|
||||
State(context, analyzer).let { state ->
|
||||
logger.trace("Validating {} references across {} class(es)...",
|
||||
context.references.size, context.classes.size)
|
||||
context.references.process { validateReference(state, it) }
|
||||
logger.trace("Reference validation completed; {} class(es) and {} message(s)",
|
||||
context.references.size, context.classes.size)
|
||||
ReferenceValidationSummary(state.context.classes, state.context.messages, state.context.classOrigins)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a message from an invalid reference and its source location.
|
||||
*/
|
||||
private fun referenceToMessage(referenceWithLocation: ReferenceWithLocation): Message {
|
||||
val (location, reference, description) = referenceWithLocation
|
||||
val referenceMessage = when {
|
||||
reference is ClassReference ->
|
||||
"Invalid reference to class ${configuration.classModule.getFormattedClassName(reference.className)}"
|
||||
reference is MemberReference && configuration.memberModule.isConstructor(reference) ->
|
||||
"Invalid reference to constructor ${memberFormatter.format(reference)}"
|
||||
reference is MemberReference && configuration.memberModule.isField(reference) ->
|
||||
"Invalid reference to field ${memberFormatter.format(reference)}"
|
||||
reference is MemberReference && configuration.memberModule.isMethod(reference) ->
|
||||
"Invalid reference to method ${memberFormatter.format(reference)}"
|
||||
else ->
|
||||
"Invalid reference to $reference"
|
||||
}
|
||||
val message = if (description.isNotBlank()) {
|
||||
"$referenceMessage, $description"
|
||||
} else {
|
||||
referenceMessage
|
||||
}
|
||||
return Message(message, Severity.ERROR, location)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a reference made from a class or class member.
|
||||
*/
|
||||
private fun validateReference(state: State, reference: EntityReference) {
|
||||
if (configuration.whitelist.matches(reference.className)) {
|
||||
// The referenced class has been whitelisted - no need to go any further.
|
||||
return
|
||||
}
|
||||
when (reference) {
|
||||
is ClassReference -> {
|
||||
logger.trace("Validating class reference {}", reference)
|
||||
val clazz = getClass(state, reference.className)
|
||||
val reason = when (clazz) {
|
||||
null -> Reason(Reason.Code.NON_EXISTENT_CLASS)
|
||||
else -> getReasonFromEntity(clazz)
|
||||
}
|
||||
if (reason != null) {
|
||||
logger.trace("Recorded invalid class reference to {}; reason = {}", reference, reason)
|
||||
state.context.messages.addAll(state.context.references.locationsFromReference(reference).map {
|
||||
referenceToMessage(ReferenceWithLocation(it, reference, reason.description))
|
||||
})
|
||||
}
|
||||
}
|
||||
is MemberReference -> {
|
||||
logger.trace("Validating member reference {}", reference)
|
||||
// Ensure that the dependent class is loaded and analyzed
|
||||
val clazz = getClass(state, reference.className)
|
||||
val member = state.context.classes.getMember(
|
||||
reference.className, reference.memberName, reference.signature
|
||||
)
|
||||
val reason = when {
|
||||
clazz == null -> Reason(Reason.Code.NON_EXISTENT_CLASS)
|
||||
member == null -> Reason(Reason.Code.NON_EXISTENT_MEMBER)
|
||||
else -> getReasonFromEntity(state, member)
|
||||
}
|
||||
if (reason != null) {
|
||||
logger.trace("Recorded invalid member reference to {}; reason = {}", reference, reason)
|
||||
state.context.messages.addAll(state.context.references.locationsFromReference(reference).map {
|
||||
referenceToMessage(ReferenceWithLocation(it, reference, reason.description))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a class from the class hierarchy by its binary name.
|
||||
*/
|
||||
private fun getClass(state: State, className: String, originClass: String? = null): ClassRepresentation? {
|
||||
val name = if (configuration.classModule.isArray(className)) {
|
||||
val arrayType = arrayTypeExtractor.find(className)?.groupValues?.get(1)
|
||||
when (arrayType) {
|
||||
null -> "java/lang/Object"
|
||||
else -> arrayType
|
||||
}
|
||||
} else {
|
||||
className
|
||||
}
|
||||
var clazz = state.context.classes[name]
|
||||
if (clazz == null) {
|
||||
logger.trace("Loading and analyzing referenced class {}...", name)
|
||||
val origin = state.context.references
|
||||
.locationsFromReference(ClassReference(name))
|
||||
.map { it.className }
|
||||
.firstOrNull() ?: originClass
|
||||
state.analyzer.analyze(name, state.context, origin)
|
||||
clazz = state.context.classes[name]
|
||||
}
|
||||
if (clazz == null) {
|
||||
logger.warn("Failed to load class {}", name)
|
||||
state.context.messages.add(Message("Referenced class not found; $name", Severity.ERROR))
|
||||
}
|
||||
clazz?.apply {
|
||||
val ancestors = listOf(superClass) + interfaces
|
||||
for (ancestor in ancestors.filter(String::isNotBlank)) {
|
||||
getClass(state, ancestor, clazz.name)
|
||||
}
|
||||
}
|
||||
return clazz
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a top-level class definition is considered safe or not.
|
||||
*/
|
||||
private fun isNonDeterministic(state: State, className: String): Boolean = when {
|
||||
configuration.whitelist.matches(className) -> false
|
||||
else -> {
|
||||
try {
|
||||
getClass(state, className)?.let {
|
||||
isNonDeterministic(it)
|
||||
} ?: true
|
||||
} catch (exception: SandboxClassLoadingException) {
|
||||
true // Failed to load the class, which means the class is non-deterministic.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a top-level class definition is considered safe or not.
|
||||
*/
|
||||
private fun isNonDeterministic(clazz: ClassRepresentation) =
|
||||
getReasonFromEntity(clazz) != null
|
||||
|
||||
/**
|
||||
* Derive what reason to give to the end-user for an invalid class.
|
||||
*/
|
||||
private fun getReasonFromEntity(clazz: ClassRepresentation): Reason? = when {
|
||||
configuration.whitelist.matches(clazz.name) -> null
|
||||
configuration.whitelist.inNamespace(clazz.name) -> Reason(Reason.Code.NOT_WHITELISTED)
|
||||
configuration.classModule.isNonDeterministic(clazz) -> Reason(Reason.Code.ANNOTATED)
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive what reason to give to the end-user for an invalid member.
|
||||
*/
|
||||
private fun getReasonFromEntity(state: State, member: Member): Reason? = when {
|
||||
configuration.whitelist.matches(member.reference) -> null
|
||||
configuration.whitelist.inNamespace(member.reference) -> Reason(Reason.Code.NOT_WHITELISTED)
|
||||
configuration.memberModule.isNonDeterministic(member) -> Reason(Reason.Code.ANNOTATED)
|
||||
else -> {
|
||||
val invalidClasses = configuration.memberModule.findReferencedClasses(member)
|
||||
.filter { isNonDeterministic(state, it) }
|
||||
if (invalidClasses.isNotEmpty()) {
|
||||
Reason(Reason.Code.INVALID_CLASS, invalidClasses.map {
|
||||
configuration.classModule.getFormattedClassName(it)
|
||||
})
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private val logger = loggerFor<ReferenceValidator>()
|
||||
|
||||
private val arrayTypeExtractor = "^\\[*L([^;]+);$".toRegex()
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -21,9 +21,9 @@ import org.objectweb.asm.ClassVisitor
|
||||
*/
|
||||
class RuleValidator(
|
||||
private val rules: List<Rule> = emptyList(),
|
||||
configuration: AnalysisConfiguration = AnalysisConfiguration(),
|
||||
configuration: AnalysisConfiguration,
|
||||
classVisitor: ClassVisitor? = null
|
||||
) : ClassAndMemberVisitor(classVisitor, configuration = configuration) {
|
||||
) : ClassAndMemberVisitor(configuration, classVisitor) {
|
||||
|
||||
/**
|
||||
* Apply the set of rules to the traversed class and record any violations.
|
||||
|
@ -2,7 +2,7 @@ package sandbox.net.corda.djvm.costing
|
||||
|
||||
import net.corda.djvm.SandboxRuntimeContext
|
||||
import net.corda.djvm.costing.RuntimeCostSummary
|
||||
import net.corda.djvm.costing.ThresholdViolationException
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
/**
|
||||
* Class for keeping a tally on various runtime metrics, like number of jumps, allocations, invocations, etc. The
|
||||
@ -24,7 +24,8 @@ object RuntimeCostAccounter {
|
||||
/**
|
||||
* The type name of the [RuntimeCostAccounter] class; referenced from instrumentors.
|
||||
*/
|
||||
const val TYPE_NAME: String = "sandbox/net/corda/djvm/costing/RuntimeCostAccounter"
|
||||
@JvmField
|
||||
val TYPE_NAME: String = Type.getInternalName(this::class.java)
|
||||
|
||||
/**
|
||||
* Known / estimated allocation costs.
|
||||
@ -35,14 +36,12 @@ object RuntimeCostAccounter {
|
||||
)
|
||||
|
||||
/**
|
||||
* Re-throw exception if it is of type [ThreadDeath] or [ThresholdViolationException].
|
||||
* Re-throw exception if it is of type [ThreadDeath] or [VirtualMachineError].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun checkCatch(exception: Throwable) {
|
||||
if (exception is ThreadDeath) {
|
||||
throw exception
|
||||
} else if (exception is ThresholdViolationException) {
|
||||
throw exception
|
||||
when (exception) {
|
||||
is ThreadDeath, is VirtualMachineError -> throw exception
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.djvm.costing
|
||||
package sandbox.net.corda.djvm.costing
|
||||
|
||||
/**
|
||||
* Exception thrown when a sandbox threshold is violated. This will kill the current thread and consequently exit the
|
||||
@ -6,6 +6,4 @@ package net.corda.djvm.costing
|
||||
*
|
||||
* @property message The description of the condition causing the problem.
|
||||
*/
|
||||
class ThresholdViolationException(
|
||||
override val message: String
|
||||
) : ThreadDeath()
|
||||
class ThresholdViolationError(override val message: String) : ThreadDeath()
|
@ -0,0 +1,10 @@
|
||||
package sandbox.net.corda.djvm.rules
|
||||
|
||||
/**
|
||||
* Exception thrown when a sandbox rule is violated at runtime.
|
||||
* This will kill the current thread and consequently exit the
|
||||
* sandbox.
|
||||
*
|
||||
* @property message The description of the condition causing the problem.
|
||||
*/
|
||||
class RuleViolationError(override val message: String) : ThreadDeath()
|
@ -16,10 +16,16 @@ import net.corda.djvm.rules.Rule
|
||||
import net.corda.djvm.source.ClassSource
|
||||
import net.corda.djvm.utilities.Discovery
|
||||
import net.corda.djvm.validation.RuleValidator
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.Type
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
abstract class TestBase {
|
||||
|
||||
@ -33,8 +39,10 @@ abstract class TestBase {
|
||||
|
||||
val BLANK = emptySet<Any>()
|
||||
|
||||
val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS)
|
||||
.toSet().distinctBy { it.javaClass }
|
||||
val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass)
|
||||
|
||||
val DETERMINISTIC_RT: Path = Paths.get(
|
||||
System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set"))
|
||||
|
||||
/**
|
||||
* Get the full name of type [T].
|
||||
@ -46,13 +54,18 @@ abstract class TestBase {
|
||||
/**
|
||||
* Default analysis configuration.
|
||||
*/
|
||||
val configuration = AnalysisConfiguration(Whitelist.MINIMAL)
|
||||
val configuration = AnalysisConfiguration(Whitelist.MINIMAL, bootstrapJar = DETERMINISTIC_RT)
|
||||
|
||||
/**
|
||||
* Default analysis context
|
||||
*/
|
||||
val context: AnalysisContext
|
||||
get() = AnalysisContext.fromConfiguration(configuration, emptyList())
|
||||
get() = AnalysisContext.fromConfiguration(configuration)
|
||||
|
||||
@After
|
||||
fun destroy() {
|
||||
configuration.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Short-hand for analysing and validating a class.
|
||||
@ -62,14 +75,15 @@ abstract class TestBase {
|
||||
noinline block: (RuleValidator.(AnalysisContext) -> Unit)
|
||||
) {
|
||||
val reader = ClassReader(T::class.java.name)
|
||||
val configuration = AnalysisConfiguration(minimumSeverityLevel = minimumSeverityLevel)
|
||||
val validator = RuleValidator(ALL_RULES, configuration)
|
||||
val context = AnalysisContext.fromConfiguration(
|
||||
configuration,
|
||||
listOf(ClassSource.fromClassName(reader.className))
|
||||
)
|
||||
validator.analyze(reader, context)
|
||||
block(validator, context)
|
||||
AnalysisConfiguration(
|
||||
minimumSeverityLevel = minimumSeverityLevel,
|
||||
classPath = listOf(DETERMINISTIC_RT)
|
||||
).use { analysisConfiguration ->
|
||||
val validator = RuleValidator(ALL_RULES, analysisConfiguration)
|
||||
val context = AnalysisContext.fromConfiguration(analysisConfiguration)
|
||||
validator.analyze(reader, context)
|
||||
block(validator, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,27 +127,26 @@ abstract class TestBase {
|
||||
}
|
||||
}
|
||||
var thrownException: Throwable? = null
|
||||
Thread {
|
||||
thread {
|
||||
try {
|
||||
val pinnedTestClasses = pinnedClasses.map(Type::getInternalName).toSet()
|
||||
val analysisConfiguration = AnalysisConfiguration(
|
||||
whitelist = whitelist,
|
||||
additionalPinnedClasses = pinnedTestClasses,
|
||||
minimumSeverityLevel = minimumSeverityLevel
|
||||
)
|
||||
SandboxRuntimeContext(SandboxConfiguration.of(
|
||||
executionProfile, rules, emitters, definitionProviders, enableTracing, analysisConfiguration
|
||||
), classSources).use {
|
||||
assertThat(runtimeCosts).areZero()
|
||||
action(this)
|
||||
AnalysisConfiguration(
|
||||
whitelist = whitelist,
|
||||
bootstrapJar = DETERMINISTIC_RT,
|
||||
additionalPinnedClasses = pinnedTestClasses,
|
||||
minimumSeverityLevel = minimumSeverityLevel
|
||||
).use { analysisConfiguration ->
|
||||
SandboxRuntimeContext(SandboxConfiguration.of(
|
||||
executionProfile, rules, emitters, definitionProviders, enableTracing, analysisConfiguration
|
||||
)).use {
|
||||
assertThat(runtimeCosts).areZero()
|
||||
action(this)
|
||||
}
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
thrownException = exception
|
||||
}
|
||||
}.apply {
|
||||
start()
|
||||
join()
|
||||
}
|
||||
}.join()
|
||||
throw thrownException ?: return
|
||||
}
|
||||
|
||||
@ -145,8 +158,12 @@ abstract class TestBase {
|
||||
/**
|
||||
* Create a new instance of a class using the sandbox class loader.
|
||||
*/
|
||||
inline fun <reified T : Callable> SandboxRuntimeContext.newCallable() =
|
||||
classLoader.loadClassAndBytes(ClassSource.fromClassName(T::class.java.name), context)
|
||||
inline fun <reified T : Callable> SandboxRuntimeContext.newCallable(): LoadedClass = loadClass<T>()
|
||||
|
||||
inline fun <reified T : Any> SandboxRuntimeContext.loadClass(): LoadedClass = loadClass(T::class.jvmName)
|
||||
|
||||
fun SandboxRuntimeContext.loadClass(className: String): LoadedClass =
|
||||
classLoader.loadClassAndBytes(ClassSource.fromClassName(className), context)
|
||||
|
||||
/**
|
||||
* Run the entry-point of the loaded [Callable] class.
|
||||
@ -164,6 +181,10 @@ abstract class TestBase {
|
||||
/**
|
||||
* Stub visitor.
|
||||
*/
|
||||
protected class Visitor : ClassVisitor(ClassAndMemberVisitor.API_VERSION)
|
||||
protected class Writer : ClassWriter(COMPUTE_FRAMES or COMPUTE_MAXS) {
|
||||
init {
|
||||
assertEquals(ClassAndMemberVisitor.API_VERSION, api)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class ClassAndMemberVisitorTest : TestBase() {
|
||||
@Test
|
||||
fun `can traverse classes`() {
|
||||
val classesVisited = mutableSetOf<ClassRepresentation>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitClass(clazz: ClassRepresentation): ClassRepresentation {
|
||||
classesVisited.add(clazz)
|
||||
return clazz
|
||||
@ -47,7 +47,7 @@ class ClassAndMemberVisitorTest : TestBase() {
|
||||
@Test
|
||||
fun `can traverse fields`() {
|
||||
val membersVisited = mutableSetOf<Member>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitField(clazz: ClassRepresentation, field: Member): Member {
|
||||
membersVisited.add(field)
|
||||
return field
|
||||
@ -77,7 +77,7 @@ class ClassAndMemberVisitorTest : TestBase() {
|
||||
@Test
|
||||
fun `can traverse methods`() {
|
||||
val membersVisited = mutableSetOf<Member>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitMethod(clazz: ClassRepresentation, method: Member): Member {
|
||||
membersVisited.add(method)
|
||||
return method
|
||||
@ -102,7 +102,7 @@ class ClassAndMemberVisitorTest : TestBase() {
|
||||
@Test
|
||||
fun `can traverse class annotations`() {
|
||||
val annotations = mutableSetOf<String>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitClassAnnotation(clazz: ClassRepresentation, descriptor: String) {
|
||||
annotations.add(descriptor)
|
||||
}
|
||||
@ -118,9 +118,21 @@ class ClassAndMemberVisitorTest : TestBase() {
|
||||
private class TestClassWithAnnotations
|
||||
|
||||
@Test
|
||||
fun `can traverse member annotations`() {
|
||||
fun `cannot traverse member annotations when reading`() {
|
||||
val annotations = mutableSetOf<String>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitMemberAnnotation(clazz: ClassRepresentation, member: Member, descriptor: String) {
|
||||
annotations.add("${member.memberName}:$descriptor")
|
||||
}
|
||||
}
|
||||
visitor.analyze<TestClassWithMemberAnnotations>(context)
|
||||
assertThat(annotations).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can traverse member annotations when writing`() {
|
||||
val annotations = mutableSetOf<String>()
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, Writer()) {
|
||||
override fun visitMemberAnnotation(clazz: ClassRepresentation, member: Member, descriptor: String) {
|
||||
annotations.add("${member.memberName}:$descriptor")
|
||||
}
|
||||
@ -146,7 +158,7 @@ class ClassAndMemberVisitorTest : TestBase() {
|
||||
@Test
|
||||
fun `can traverse class sources`() {
|
||||
val sources = mutableSetOf<String>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitSource(clazz: ClassRepresentation, source: String) {
|
||||
sources.add(source)
|
||||
}
|
||||
@ -160,9 +172,21 @@ class ClassAndMemberVisitorTest : TestBase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can traverse instructions`() {
|
||||
fun `does not traverse instructions when reading`() {
|
||||
val instructions = mutableSetOf<Pair<Member, Instruction>>()
|
||||
val visitor = object : ClassAndMemberVisitor() {
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, null) {
|
||||
override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) {
|
||||
instructions.add(Pair(method, instruction))
|
||||
}
|
||||
}
|
||||
visitor.analyze<TestClassWithCode>(context)
|
||||
assertThat(instructions).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can traverse instructions when writing`() {
|
||||
val instructions = mutableSetOf<Pair<Member, Instruction>>()
|
||||
val visitor = object : ClassAndMemberVisitor(configuration, Writer()) {
|
||||
override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) {
|
||||
instructions.add(Pair(method, instruction))
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
package net.corda.djvm.analysis
|
||||
|
||||
import net.corda.djvm.TestBase
|
||||
import net.corda.djvm.execution.SandboxedRunnable
|
||||
import net.corda.djvm.validation.ReferenceValidator
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class ReferenceValidatorTest : TestBase() {
|
||||
|
||||
private fun validator(whitelist: Whitelist = Whitelist.MINIMAL) =
|
||||
ReferenceValidator(AnalysisConfiguration(whitelist))
|
||||
|
||||
@Test
|
||||
fun `can validate when there are no references`() = analyze { context ->
|
||||
analyze<EmptyRunnable>(context)
|
||||
val (_, messages) = validator().validate(context, this)
|
||||
assertThat(messages.count).isEqualTo(0)
|
||||
}
|
||||
|
||||
private class EmptyRunnable : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can validate when there are references`() = analyze { context ->
|
||||
analyze<RunnableWithReferences>(context)
|
||||
analyze<TestRandom>(context)
|
||||
val (_, messages) = validator().validate(context, this)
|
||||
assertThat(messages.count).isEqualTo(0)
|
||||
}
|
||||
|
||||
private class RunnableWithReferences : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
return TestRandom().nextInt()
|
||||
}
|
||||
}
|
||||
|
||||
private class TestRandom {
|
||||
external fun nextInt(): Int
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can validate when there are transient references`() = analyze { context ->
|
||||
analyze<RunnableWithTransientReferences>(context)
|
||||
analyze<ReferencedClass>(context)
|
||||
analyze<TestRandom>(context)
|
||||
val (_, messages) = validator().validate(context, this)
|
||||
assertThat(messages.count).isEqualTo(0)
|
||||
}
|
||||
|
||||
private class RunnableWithTransientReferences : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
return ReferencedClass().test()
|
||||
}
|
||||
}
|
||||
|
||||
private class ReferencedClass {
|
||||
fun test(): Int {
|
||||
return TestRandom().nextInt()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +1,31 @@
|
||||
package net.corda.djvm.assertions
|
||||
|
||||
import net.corda.djvm.rewiring.LoadedClass
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.assertj.core.api.Assertions.*
|
||||
|
||||
class AssertiveClassWithByteCode(private val loadedClass: LoadedClass) {
|
||||
|
||||
fun isSandboxed(): AssertiveClassWithByteCode {
|
||||
Assertions.assertThat(loadedClass.type.name).startsWith("sandbox.")
|
||||
assertThat(loadedClass.type.name).startsWith("sandbox.")
|
||||
return this
|
||||
}
|
||||
|
||||
fun hasNotBeenModified(): AssertiveClassWithByteCode {
|
||||
Assertions.assertThat(loadedClass.byteCode.isModified)
|
||||
assertThat(loadedClass.byteCode.isModified)
|
||||
.`as`("Byte code has been modified")
|
||||
.isEqualTo(false)
|
||||
return this
|
||||
}
|
||||
|
||||
fun hasBeenModified(): AssertiveClassWithByteCode {
|
||||
Assertions.assertThat(loadedClass.byteCode.isModified)
|
||||
.`as`("Byte code has been modified")
|
||||
assertThat(loadedClass.byteCode.isModified)
|
||||
.`as`("Byte code has not been modified")
|
||||
.isEqualTo(true)
|
||||
return this
|
||||
}
|
||||
|
||||
fun hasClassName(className: String): AssertiveClassWithByteCode {
|
||||
assertThat(loadedClass.type.name).isEqualTo(className)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ open class AssertiveReferenceMap(private val references: ReferenceMap) {
|
||||
|
||||
fun hasCount(count: Int): AssertiveReferenceMap {
|
||||
val allReferences = references.joinToString("\n") { " - $it" }
|
||||
Assertions.assertThat(references.size)
|
||||
Assertions.assertThat(references.numberOfReferences)
|
||||
.overridingErrorMessage("Expected $count reference(s), found:\n$allReferences")
|
||||
.isEqualTo(count)
|
||||
return this
|
||||
|
@ -21,7 +21,7 @@ class ClassMutatorTest : TestBase() {
|
||||
}
|
||||
}
|
||||
val context = context
|
||||
val mutator = ClassMutator(Visitor(), configuration, listOf(definitionProvider))
|
||||
val mutator = ClassMutator(Writer(), configuration, listOf(definitionProvider))
|
||||
mutator.analyze<TestClass>(context)
|
||||
assertThat(hasProvidedDefinition).isTrue()
|
||||
assertThat(context.classes.get<TestClass>().access or ACC_STRICT).isNotEqualTo(0)
|
||||
@ -39,7 +39,7 @@ class ClassMutatorTest : TestBase() {
|
||||
}
|
||||
}
|
||||
val context = context
|
||||
val mutator = ClassMutator(Visitor(), configuration, listOf(definitionProvider))
|
||||
val mutator = ClassMutator(Writer(), configuration, listOf(definitionProvider))
|
||||
mutator.analyze<TestClassWithMembers>(context)
|
||||
assertThat(hasProvidedDefinition).isTrue()
|
||||
for (member in context.classes.get<TestClassWithMembers>().members.values) {
|
||||
@ -67,7 +67,7 @@ class ClassMutatorTest : TestBase() {
|
||||
}
|
||||
}
|
||||
val context = context
|
||||
val mutator = ClassMutator(Visitor(), configuration, emitters = listOf(emitter))
|
||||
val mutator = ClassMutator(Writer(), configuration, emitters = listOf(emitter))
|
||||
mutator.analyze<TestClassWithMembers>(context)
|
||||
assertThat(hasEmittedCode).isTrue()
|
||||
assertThat(shouldPreventDefault).isTrue()
|
||||
|
@ -7,6 +7,7 @@ import org.junit.Test
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes.NEW
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
class EmitterModuleTest : TestBase() {
|
||||
|
||||
@ -14,15 +15,15 @@ class EmitterModuleTest : TestBase() {
|
||||
fun `can emit code to method body`() {
|
||||
var hasEmittedTypeInstruction = false
|
||||
val methodVisitor = object : MethodVisitor(ClassAndMemberVisitor.API_VERSION) {
|
||||
override fun visitTypeInsn(opcode: Int, type: String?) {
|
||||
if (opcode == NEW && type == java.lang.String::class.java.name) {
|
||||
override fun visitTypeInsn(opcode: Int, type: String) {
|
||||
if (opcode == NEW && type == Type.getInternalName(java.lang.String::class.java)) {
|
||||
hasEmittedTypeInstruction = true
|
||||
}
|
||||
}
|
||||
}
|
||||
val visitor = object : ClassVisitor(ClassAndMemberVisitor.API_VERSION) {
|
||||
override fun visitMethod(
|
||||
access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?
|
||||
access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?
|
||||
): MethodVisitor {
|
||||
return methodVisitor
|
||||
}
|
||||
|
@ -3,12 +3,13 @@ package net.corda.djvm.costing
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
|
||||
class RuntimeCostTest {
|
||||
|
||||
@Test
|
||||
fun `can increment cost`() {
|
||||
val cost = RuntimeCost(10, { "failed" })
|
||||
val cost = RuntimeCost(10) { "failed" }
|
||||
cost.increment(1)
|
||||
assertThat(cost.value).isEqualTo(1)
|
||||
}
|
||||
@ -16,8 +17,8 @@ class RuntimeCostTest {
|
||||
@Test
|
||||
fun `cannot increment cost beyond threshold`() {
|
||||
Thread {
|
||||
val cost = RuntimeCost(10, { "failed in ${it.name}" })
|
||||
assertThatExceptionOfType(ThresholdViolationException::class.java)
|
||||
val cost = RuntimeCost(10) { "failed in ${it.name}" }
|
||||
assertThatExceptionOfType(ThresholdViolationError::class.java)
|
||||
.isThrownBy { cost.increment(11) }
|
||||
.withMessage("failed in Foo")
|
||||
assertThat(cost.value).isEqualTo(11)
|
||||
|
@ -6,13 +6,15 @@ import foo.bar.sandbox.toNumber
|
||||
import net.corda.djvm.TestBase
|
||||
import net.corda.djvm.analysis.Whitelist
|
||||
import net.corda.djvm.assertions.AssertionExtensions.withProblem
|
||||
import net.corda.djvm.costing.ThresholdViolationException
|
||||
import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
import sandbox.net.corda.djvm.rules.RuleViolationError
|
||||
import java.nio.file.Files
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
|
||||
class SandboxExecutorTest : TestBase() {
|
||||
|
||||
@ -24,8 +26,8 @@ class SandboxExecutorTest : TestBase() {
|
||||
assertThat(result).isEqualTo("sandbox")
|
||||
}
|
||||
|
||||
class TestSandboxedRunnable : SandboxedRunnable<Int, String> {
|
||||
override fun run(input: Int): String? {
|
||||
class TestSandboxedRunnable : Function<Int, String> {
|
||||
override fun apply(input: Int): String {
|
||||
return "sandbox"
|
||||
}
|
||||
}
|
||||
@ -42,8 +44,8 @@ class SandboxExecutorTest : TestBase() {
|
||||
.withMessageContaining("Contract constraint violated")
|
||||
}
|
||||
|
||||
class Contract : SandboxedRunnable<Transaction?, Unit> {
|
||||
override fun run(input: Transaction?) {
|
||||
class Contract : Function<Transaction?, Unit> {
|
||||
override fun apply(input: Transaction?) {
|
||||
throw IllegalArgumentException("Contract constraint violated")
|
||||
}
|
||||
}
|
||||
@ -58,8 +60,8 @@ class SandboxExecutorTest : TestBase() {
|
||||
assertThat(result).isEqualTo(0xfed_c0de + 2)
|
||||
}
|
||||
|
||||
class TestObjectHashCode : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestObjectHashCode : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
val obj = Object()
|
||||
val hash1 = obj.hashCode()
|
||||
val hash2 = obj.hashCode()
|
||||
@ -76,8 +78,8 @@ class SandboxExecutorTest : TestBase() {
|
||||
assertThat(result).isEqualTo(0xfed_c0de + 1)
|
||||
}
|
||||
|
||||
class TestObjectHashCodeWithHierarchy : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestObjectHashCodeWithHierarchy : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
val obj = MyObject()
|
||||
return obj.hashCode()
|
||||
}
|
||||
@ -91,9 +93,9 @@ class SandboxExecutorTest : TestBase() {
|
||||
.withMessageContaining("terminated due to excessive use of looping")
|
||||
}
|
||||
|
||||
class TestThresholdBreach : SandboxedRunnable<Int, Int> {
|
||||
class TestThresholdBreach : Function<Int, Int> {
|
||||
private var x = 0
|
||||
override fun run(input: Int): Int? {
|
||||
override fun apply(input: Int): Int {
|
||||
for (i in 0..1_000_000) {
|
||||
x += 1
|
||||
}
|
||||
@ -109,8 +111,8 @@ class SandboxExecutorTest : TestBase() {
|
||||
.withCauseInstanceOf(StackOverflowError::class.java)
|
||||
}
|
||||
|
||||
class TestStackOverflow : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestStackOverflow : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return a()
|
||||
}
|
||||
|
||||
@ -124,11 +126,12 @@ class SandboxExecutorTest : TestBase() {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestKotlinMetaClasses>(0) }
|
||||
.withMessageContaining("java/util/Random.<clinit>(): Disallowed reference to reflection API; sun.misc.Unsafe.getUnsafe()")
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Disallowed reference to reflection API")
|
||||
}
|
||||
|
||||
class TestKotlinMetaClasses : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestKotlinMetaClasses : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
val someNumber = testRandom()
|
||||
return "12345".toNumber() * someNumber
|
||||
}
|
||||
@ -139,30 +142,32 @@ class SandboxExecutorTest : TestBase() {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestNonDeterministicCode>(0) }
|
||||
.withCauseInstanceOf(SandboxClassLoadingException::class.java)
|
||||
.withProblem("java/util/Random.<clinit>(): Disallowed reference to reflection API; sun.misc.Unsafe.getUnsafe()")
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withProblem("Disallowed reference to reflection API")
|
||||
}
|
||||
|
||||
class TestNonDeterministicCode : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestNonDeterministicCode : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return Random().nextInt()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot execute runnable that catches ThreadDeath`() = sandbox(DEFAULT) {
|
||||
TestCatchThreadDeath().apply {
|
||||
assertThat(apply(0)).isEqualTo(1)
|
||||
}
|
||||
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestCatchThreadDeath>(0) }
|
||||
.withCauseInstanceOf(SandboxClassLoadingException::class.java)
|
||||
.withMessageContaining("Disallowed catch of ThreadDeath exception")
|
||||
.withMessageContaining(TestCatchThreadDeath::class.java.simpleName)
|
||||
.withCauseExactlyInstanceOf(ThreadDeath::class.java)
|
||||
}
|
||||
|
||||
class TestCatchThreadDeath : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestCatchThreadDeath : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
0
|
||||
throw ThreadDeath()
|
||||
} catch (exception: ThreadDeath) {
|
||||
1
|
||||
}
|
||||
@ -170,20 +175,46 @@ class SandboxExecutorTest : TestBase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot execute runnable that catches ThresholdViolationException`() = sandbox(DEFAULT) {
|
||||
fun `cannot execute runnable that catches ThresholdViolationError`() = sandbox(DEFAULT) {
|
||||
TestCatchThresholdViolationError().apply {
|
||||
assertThat(apply(0)).isEqualTo(1)
|
||||
}
|
||||
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestCatchThresholdViolationException>(0) }
|
||||
.withCauseInstanceOf(SandboxClassLoadingException::class.java)
|
||||
.withMessageContaining("Disallowed catch of threshold violation exception")
|
||||
.withMessageContaining(TestCatchThresholdViolationException::class.java.simpleName)
|
||||
.isThrownBy { contractExecutor.run<TestCatchThresholdViolationError>(0) }
|
||||
.withCauseExactlyInstanceOf(ThresholdViolationError::class.java)
|
||||
.withMessageContaining("Can't catch this!")
|
||||
}
|
||||
|
||||
class TestCatchThresholdViolationException : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestCatchThresholdViolationError : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
0
|
||||
} catch (exception: ThresholdViolationException) {
|
||||
throw ThresholdViolationError("Can't catch this!")
|
||||
} catch (exception: ThresholdViolationError) {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot execute runnable that catches RuleViolationError`() = sandbox(DEFAULT) {
|
||||
TestCatchRuleViolationError().apply {
|
||||
assertThat(apply(0)).isEqualTo(1)
|
||||
}
|
||||
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestCatchRuleViolationError>(0) }
|
||||
.withCauseExactlyInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Can't catch this!")
|
||||
}
|
||||
|
||||
class TestCatchRuleViolationError : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
throw RuleViolationError("Can't catch this!")
|
||||
} catch (exception: RuleViolationError) {
|
||||
1
|
||||
}
|
||||
}
|
||||
@ -209,12 +240,12 @@ class SandboxExecutorTest : TestBase() {
|
||||
fun `cannot catch ThreadDeath`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestCatchThrowableErrorAndThreadDeath>(3) }
|
||||
.isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(3) }
|
||||
.withCauseInstanceOf(ThreadDeath::class.java)
|
||||
}
|
||||
|
||||
class TestCatchThrowableAndError : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestCatchThrowableAndError : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
when (input) {
|
||||
1 -> throw Throwable()
|
||||
@ -229,13 +260,27 @@ class SandboxExecutorTest : TestBase() {
|
||||
}
|
||||
}
|
||||
|
||||
class TestCatchThrowableErrorAndThreadDeath : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestCatchThrowableErrorsAndThreadDeath : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return try {
|
||||
when (input) {
|
||||
1 -> throw Throwable()
|
||||
2 -> throw Error()
|
||||
3 -> throw ThreadDeath()
|
||||
3 -> try {
|
||||
throw ThreadDeath()
|
||||
} catch (ex: ThreadDeath) {
|
||||
3
|
||||
}
|
||||
4 -> try {
|
||||
throw StackOverflowError("FAKE OVERFLOW!")
|
||||
} catch (ex: StackOverflowError) {
|
||||
4
|
||||
}
|
||||
5 -> try {
|
||||
throw OutOfMemoryError("FAKE OOM!")
|
||||
} catch (ex: OutOfMemoryError) {
|
||||
5
|
||||
}
|
||||
else -> 0
|
||||
}
|
||||
} catch (exception: Error) {
|
||||
@ -246,6 +291,24 @@ class SandboxExecutorTest : TestBase() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot catch stack-overflow error`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(4) }
|
||||
.withCauseInstanceOf(StackOverflowError::class.java)
|
||||
.withMessageContaining("FAKE OVERFLOW!")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot catch out-of-memory error`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(5) }
|
||||
.withCauseInstanceOf(OutOfMemoryError::class.java)
|
||||
.withMessageContaining("FAKE OOM!")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot persist state across sessions`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
@ -256,8 +319,8 @@ class SandboxExecutorTest : TestBase() {
|
||||
.isEqualTo(1)
|
||||
}
|
||||
|
||||
class TestStatePersistence : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestStatePersistence : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
ReferencedClass.value += 1
|
||||
return ReferencedClass.value
|
||||
}
|
||||
@ -274,11 +337,11 @@ class SandboxExecutorTest : TestBase() {
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestIO>(0) }
|
||||
.withCauseInstanceOf(SandboxClassLoadingException::class.java)
|
||||
.withMessageContaining("Files.walk(Path, Integer, FileVisitOption[]): Disallowed dynamic invocation in method")
|
||||
.withMessageContaining("Class file not found; java/nio/file/Files.class")
|
||||
}
|
||||
|
||||
class TestIO : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestIO : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
val file = Files.createTempFile("test", ".dat")
|
||||
Files.newBufferedWriter(file).use {
|
||||
it.write("Hello world!")
|
||||
@ -292,14 +355,13 @@ class SandboxExecutorTest : TestBase() {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestReflection>(0) }
|
||||
.withCauseInstanceOf(SandboxClassLoadingException::class.java)
|
||||
.withMessageContaining("Disallowed reference to reflection API")
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Disallowed reference to API;")
|
||||
.withMessageContaining("java.lang.Class.newInstance()")
|
||||
.withMessageContaining("java.lang.reflect.Method.invoke(Object, Object[])")
|
||||
}
|
||||
|
||||
class TestReflection : SandboxedRunnable<Int, Int> {
|
||||
override fun run(input: Int): Int? {
|
||||
class TestReflection : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
val clazz = Object::class.java
|
||||
val obj = clazz.newInstance()
|
||||
val result = clazz.methods.first().invoke(obj)
|
||||
@ -307,4 +369,150 @@ class SandboxExecutorTest : TestBase() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load and execute code that uses notify()`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestMonitors>(1) }
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Disallowed reference to API;")
|
||||
.withMessageContaining("java.lang.Object.notify()")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load and execute code that uses notifyAll()`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestMonitors>(2) }
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Disallowed reference to API;")
|
||||
.withMessageContaining("java.lang.Object.notifyAll()")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load and execute code that uses wait()`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestMonitors>(3) }
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Disallowed reference to API;")
|
||||
.withMessageContaining("java.lang.Object.wait()")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load and execute code that uses wait(long)`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestMonitors>(4) }
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Disallowed reference to API;")
|
||||
.withMessageContaining("java.lang.Object.wait(Long)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load and execute code that uses wait(long,int)`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestMonitors>(5) }
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Disallowed reference to API;")
|
||||
.withMessageContaining("java.lang.Object.wait(Long, Integer)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `code after forbidden APIs is intact`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
|
||||
assertThat(contractExecutor.run<TestMonitors>(0).result)
|
||||
.isEqualTo("unknown")
|
||||
}
|
||||
|
||||
class TestMonitors : Function<Int, String> {
|
||||
override fun apply(input: Int): String {
|
||||
return synchronized(this) {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
val javaObject = this as java.lang.Object
|
||||
when(input) {
|
||||
1 -> {
|
||||
javaObject.notify()
|
||||
"notify"
|
||||
}
|
||||
2 -> {
|
||||
javaObject.notifyAll()
|
||||
"notifyAll"
|
||||
}
|
||||
3 -> {
|
||||
javaObject.wait()
|
||||
"wait"
|
||||
}
|
||||
4 -> {
|
||||
javaObject.wait(100)
|
||||
"wait(100)"
|
||||
}
|
||||
5 -> {
|
||||
javaObject.wait(100, 10)
|
||||
"wait(100, 10)"
|
||||
}
|
||||
else -> "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load and execute code that has a native method`() = sandbox(DEFAULT) {
|
||||
assertThatExceptionOfType(UnsatisfiedLinkError::class.java)
|
||||
.isThrownBy { TestNativeMethod().apply(0) }
|
||||
.withMessageContaining("TestNativeMethod.evilDeeds()I")
|
||||
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
assertThatExceptionOfType(SandboxException::class.java)
|
||||
.isThrownBy { contractExecutor.run<TestNativeMethod>(0) }
|
||||
.withCauseInstanceOf(RuleViolationError::class.java)
|
||||
.withMessageContaining("Native method has been deleted")
|
||||
}
|
||||
|
||||
class TestNativeMethod : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return evilDeeds()
|
||||
}
|
||||
|
||||
private external fun evilDeeds(): Int
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check arrays still work`() = sandbox(DEFAULT) {
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Array<Int>>(configuration)
|
||||
contractExecutor.run<TestArray>(100).apply {
|
||||
assertThat(result).isEqualTo(arrayOf(100))
|
||||
}
|
||||
}
|
||||
|
||||
class TestArray : Function<Int, Array<Int>> {
|
||||
override fun apply(input: Int): Array<Int> {
|
||||
return listOf(input).toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load and execute class that has finalize`() = sandbox(DEFAULT) {
|
||||
assertThatExceptionOfType(UnsupportedOperationException::class.java)
|
||||
.isThrownBy { TestFinalizeMethod().apply(100) }
|
||||
.withMessageContaining("Very Bad Thing")
|
||||
|
||||
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
|
||||
contractExecutor.run<TestFinalizeMethod>(100).apply {
|
||||
assertThat(result).isEqualTo(100)
|
||||
}
|
||||
}
|
||||
|
||||
class TestFinalizeMethod : Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
finalize()
|
||||
return input
|
||||
}
|
||||
|
||||
private fun finalize() {
|
||||
throw UnsupportedOperationException("Very Bad Thing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,11 @@ import foo.bar.sandbox.Empty
|
||||
import foo.bar.sandbox.StrictFloat
|
||||
import net.corda.djvm.TestBase
|
||||
import net.corda.djvm.assertions.AssertionExtensions.assertThat
|
||||
import net.corda.djvm.costing.ThresholdViolationException
|
||||
import net.corda.djvm.execution.ExecutionProfile
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
import sandbox.net.corda.djvm.costing.ThresholdViolationError
|
||||
import java.nio.file.Paths
|
||||
|
||||
class ClassRewriterTest : TestBase() {
|
||||
|
||||
@ -45,7 +46,7 @@ class ClassRewriterTest : TestBase() {
|
||||
val callable = newCallable<B>()
|
||||
assertThat(callable).hasBeenModified()
|
||||
assertThat(callable).isSandboxed()
|
||||
assertThatExceptionOfType(ThresholdViolationException::class.java).isThrownBy {
|
||||
assertThatExceptionOfType(ThresholdViolationError::class.java).isThrownBy {
|
||||
callable.createAndInvoke()
|
||||
}.withMessageContaining("terminated due to excessive use of looping")
|
||||
assertThat(runtimeCosts)
|
||||
@ -61,4 +62,44 @@ class ClassRewriterTest : TestBase() {
|
||||
callable.createAndInvoke()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load a Java API that still exists in Java runtime`() = sandbox(DEFAULT) {
|
||||
assertThat(loadClass<MutableList<*>>())
|
||||
.hasClassName("sandbox.java.util.List")
|
||||
.hasBeenModified()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot load a Java API that was deleted from Java runtime`() = sandbox(DEFAULT) {
|
||||
assertThatExceptionOfType(SandboxClassLoadingException::class.java)
|
||||
.isThrownBy { loadClass<Paths>() }
|
||||
.withMessageContaining("Class file not found; java/nio/file/Paths.class")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `load internal Sun class that still exists in Java runtime`() = sandbox(DEFAULT) {
|
||||
assertThat(loadClass<sun.misc.Unsafe>())
|
||||
.hasClassName("sandbox.sun.misc.Unsafe")
|
||||
.hasBeenModified()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot load internal Sun class that was deleted from Java runtime`() = sandbox(DEFAULT) {
|
||||
assertThatExceptionOfType(SandboxClassLoadingException::class.java)
|
||||
.isThrownBy { loadClass<sun.misc.Timer>() }
|
||||
.withMessageContaining("Class file not found; sun/misc/Timer.class")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load local class`() = sandbox(DEFAULT) {
|
||||
assertThat(loadClass<Example>())
|
||||
.hasClassName("sandbox.net.corda.djvm.rewiring.ClassRewriterTest\$Example")
|
||||
.hasBeenModified()
|
||||
}
|
||||
|
||||
class Example : java.util.function.Function<Int, Int> {
|
||||
override fun apply(input: Int): Int {
|
||||
return input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,19 +9,6 @@ import java.util.*
|
||||
|
||||
class ReferenceExtractorTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun `can find method references`() = validate<A> { context ->
|
||||
assertThat(context.references)
|
||||
.hasClass("java/util/Random")
|
||||
.withLocationCount(1)
|
||||
.hasMember("java/lang/Object", "<init>", "()V")
|
||||
.withLocationCount(1)
|
||||
.hasMember("java/util/Random", "<init>", "()V")
|
||||
.withLocationCount(1)
|
||||
.hasMember("java/util/Random", "nextInt", "()I")
|
||||
.withLocationCount(1)
|
||||
}
|
||||
|
||||
class A : Callable {
|
||||
override fun call() {
|
||||
synchronized(this) {
|
||||
@ -30,21 +17,6 @@ class ReferenceExtractorTest : TestBase() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can find field references`() = validate<B> { context ->
|
||||
assertThat(context.references)
|
||||
.hasMember(Type.getInternalName(B::class.java), "foo", "Ljava/lang/String;")
|
||||
}
|
||||
|
||||
class B {
|
||||
@JvmField
|
||||
val foo: String = ""
|
||||
|
||||
fun test(): String {
|
||||
return foo
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can find class references`() = validate<C> { context ->
|
||||
assertThat(context.references)
|
||||
|
@ -42,8 +42,7 @@ class RuleValidatorTest : TestBase() {
|
||||
assertThat(context.messages)
|
||||
.hasErrorCount(0)
|
||||
.hasWarningCount(0)
|
||||
.hasInfoCount(1)
|
||||
.withMessage("Stripped monitoring instruction")
|
||||
.hasInfoCount(0)
|
||||
.hasTraceCount(4)
|
||||
.withMessage("Synchronization specifier will be ignored")
|
||||
.withMessage("Strict floating-point arithmetic will be applied")
|
||||
|
@ -67,7 +67,7 @@ class SourceClassLoaderTest {
|
||||
val (first, second) = this
|
||||
val directory = first.parent
|
||||
val classLoader = SourceClassLoader(listOf(directory), classResolver)
|
||||
assertThat(classLoader.resolvedUrls).anySatisfy {
|
||||
assertThat(classLoader.urLs).anySatisfy {
|
||||
assertThat(it).isEqualTo(first.toUri().toURL())
|
||||
}.anySatisfy {
|
||||
assertThat(it).isEqualTo(second.toUri().toURL())
|
||||
|
@ -7,6 +7,8 @@ release, see :doc:`upgrade-notes`.
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
* Introduce minimum and target platform version for CorDapps.
|
||||
|
||||
* New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call.
|
||||
|
||||
* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive.
|
||||
|
@ -189,3 +189,30 @@ CorDapp configuration can be accessed from ``CordappContext::config`` whenever a
|
||||
|
||||
There is an example project that demonstrates in ``samples`` called ``cordapp-configuration`` and API documentation in
|
||||
`<api/kotlin/corda/net.corda.core.cordapp/index.html>`_.
|
||||
|
||||
|
||||
Minimum and target platform version
|
||||
-----------------------------------
|
||||
|
||||
CorDapps can advertise their minimum and target platform version. The minimum platform version indicates that a node has to run at least this version in order to be able to run this CorDapp. The target platform version indicates that a CorDapp was tested with this version of the Corda Platform and should be run at this API level if possible. It provides a means of maintaining behavioural compatibility for the cases where the platform's behaviour has changed. These attributes are specified in the JAR manifest of the CorDapp, for example:
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
'Min-Platform-Version': 3
|
||||
'Target-Platform-Version': 4
|
||||
|
||||
|
||||
In gradle, this can be achieved by modifying the jar task as shown in this example:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Min-Platform-Version': 3
|
||||
'Target-Platform-Version': 4
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,4 @@ and the release version - a change in the major, minor or patch values may or ma
|
||||
The Platform Version is part of the node's ``NodeInfo`` object, which is available from the ``ServiceHub``. This enables
|
||||
a CorDapp to find out which version it's running on and determine whether a desired feature is available. When a node
|
||||
registers with the Network Map Service it will use the node's Platform Version to enforce a minimum version requirement
|
||||
for the network.
|
||||
|
||||
.. note:: A future release may introduce the concept of a target platform version, which would be similar to Android's
|
||||
``targetSdkVersion``, and would provide a means of maintaining behavioural compatibility for the cases where the
|
||||
platform's behaviour has changed.
|
||||
for the network.
|
@ -35,7 +35,9 @@ import kotlin.concurrent.withLock
|
||||
* The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksProxyConfig? = null, maxMessageSize: Int, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager {
|
||||
class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksProxyConfig? = null, maxMessageSize: Int,
|
||||
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
|
||||
private val bridgeMetricsService: BridgeMetricsService? = null) : BridgeManager {
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
private val queueNamesToBridgesMap = mutableMapOf<String, MutableList<AMQPBridge>>()
|
||||
@ -72,10 +74,11 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
*/
|
||||
private class AMQPBridge(val queueName: String,
|
||||
val targets: List<NetworkHostAndPort>,
|
||||
private val legalNames: Set<CordaX500Name>,
|
||||
val legalNames: Set<CordaX500Name>,
|
||||
private val amqpConfig: AMQPConfiguration,
|
||||
sharedEventGroup: EventLoopGroup,
|
||||
private val artemis: ArtemisSessionProvider) {
|
||||
private val artemis: ArtemisSessionProvider,
|
||||
private val bridgeMetricsService: BridgeMetricsService?) {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
@ -111,7 +114,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
|
||||
fun start() {
|
||||
logInfoWithMDC("Create new AMQP bridge")
|
||||
connectedSubscription = amqpClient.onConnection.subscribe({ x -> onSocketConnected(x.connected) })
|
||||
connectedSubscription = amqpClient.onConnection.subscribe { x -> onSocketConnected(x.connected) }
|
||||
amqpClient.start()
|
||||
}
|
||||
|
||||
@ -143,6 +146,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
synchronized(artemis) {
|
||||
if (connected) {
|
||||
logInfoWithMDC("Bridge Connected")
|
||||
bridgeMetricsService?.bridgeConnected(targets, legalNames)
|
||||
val sessionFactory = artemis.started!!.sessionFactory
|
||||
val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||
this.session = session
|
||||
@ -152,6 +156,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
session.start()
|
||||
} else {
|
||||
logInfoWithMDC("Bridge Disconnected")
|
||||
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
|
||||
consumer?.apply {
|
||||
if (!isClosed) {
|
||||
close()
|
||||
@ -171,8 +176,10 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
|
||||
private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) {
|
||||
if (artemisMessage.bodySize > amqpConfig.maxMessageSize) {
|
||||
logWarnWithMDC("Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " +
|
||||
"dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}")
|
||||
val msg = "Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " +
|
||||
"dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}"
|
||||
logWarnWithMDC(msg)
|
||||
bridgeMetricsService?.packetDropEvent(artemisMessage, msg)
|
||||
// Ack the message to prevent same message being sent to us again.
|
||||
artemisMessage.acknowledge()
|
||||
return
|
||||
@ -208,6 +215,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
}
|
||||
}
|
||||
amqpClient.write(sendableMessage)
|
||||
bridgeMetricsService?.packetAcceptedEvent(sendableMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,8 +227,9 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
return
|
||||
}
|
||||
}
|
||||
val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!)
|
||||
val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!, bridgeMetricsService)
|
||||
bridges += newBridge
|
||||
bridgeMetricsService?.bridgeCreated(targets, legalNames)
|
||||
newBridge
|
||||
}
|
||||
newBridge.start()
|
||||
@ -237,6 +246,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
queueNamesToBridgesMap.remove(queueName)
|
||||
}
|
||||
bridge.stop()
|
||||
bridgeMetricsService?.bridgeDestroyed(bridge.targets, bridge.legalNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,12 +26,13 @@ import java.util.*
|
||||
class BridgeControlListener(val config: MutualSslConfiguration,
|
||||
socksProxyConfig: SocksProxyConfig? = null,
|
||||
maxMessageSize: Int,
|
||||
val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable {
|
||||
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
|
||||
bridgeMetricsService: BridgeMetricsService? = null) : AutoCloseable {
|
||||
private val bridgeId: String = UUID.randomUUID().toString()
|
||||
private val bridgeControlQueue = "$BRIDGE_CONTROL.$bridgeId"
|
||||
private val bridgeNotifyQueue = "$BRIDGE_NOTIFY.$bridgeId"
|
||||
private val bridgeManager: BridgeManager = AMQPBridgeManager(config, socksProxyConfig, maxMessageSize,
|
||||
artemisMessageClientFactory)
|
||||
artemisMessageClientFactory, bridgeMetricsService)
|
||||
private val validInboundQueues = mutableSetOf<String>()
|
||||
private var artemis: ArtemisSessionProvider? = null
|
||||
private var controlConsumer: ClientConsumer? = null
|
||||
|
@ -0,0 +1,15 @@
|
||||
package net.corda.nodeapi.internal.bridging
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
|
||||
interface BridgeMetricsService {
|
||||
fun bridgeCreated(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
|
||||
fun bridgeConnected(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
|
||||
fun packetDropEvent(artemisMessage: ClientMessage, msg: String)
|
||||
fun packetAcceptedEvent(sendableMessage: SendableMessage)
|
||||
fun bridgeDisconnected(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
|
||||
fun bridgeDestroyed(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
|
||||
}
|
@ -15,6 +15,7 @@ import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
@ -110,7 +111,7 @@ class AttachmentsClassLoaderStaticContractTests {
|
||||
val cordapps = cordappsForPackages(packages)
|
||||
return testDirectory().let { directory ->
|
||||
cordapps.packageInDirectory(directory)
|
||||
JarScanningCordappLoader.fromDirectories(listOf(directory))
|
||||
JarScanningCordappLoader.fromDirectories(listOf(directory), VersionInfo.UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,10 +25,10 @@ import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import net.corda.testing.core.MAX_MESSAGE_SIZE
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import net.corda.testing.internal.DEV_INTERMEDIATE_CA
|
||||
import net.corda.testing.internal.DEV_ROOT_CA
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter
|
||||
@ -62,6 +62,7 @@ import javax.ws.rs.Path
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.core.Response
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CertificateRevocationListNodeTests {
|
||||
@Rule
|
||||
@ -81,6 +82,32 @@ class CertificateRevocationListNodeTests {
|
||||
|
||||
private abstract class AbstractNodeConfiguration : NodeConfiguration
|
||||
|
||||
companion object {
|
||||
fun createRevocationList(clrServer: CrlServer, signatureAlgorithm: String, caCertificate: X509Certificate,
|
||||
caPrivateKey: PrivateKey,
|
||||
endpoint: String,
|
||||
indirect: Boolean,
|
||||
vararg serialNumbers: BigInteger): X509CRL {
|
||||
println("Generating CRL for $endpoint")
|
||||
val builder = JcaX509v2CRLBuilder(caCertificate.subjectX500Principal, Date(System.currentTimeMillis() - 1.minutes.toMillis()))
|
||||
val extensionUtils = JcaX509ExtensionUtils()
|
||||
builder.addExtension(Extension.authorityKeyIdentifier,
|
||||
false, extensionUtils.createAuthorityKeyIdentifier(caCertificate))
|
||||
val issuingDistPointName = GeneralName(
|
||||
GeneralName.uniformResourceIdentifier,
|
||||
"http://${clrServer.hostAndPort.host}:${clrServer.hostAndPort.port}/crl/$endpoint")
|
||||
// This is required and needs to match the certificate settings with respect to being indirect
|
||||
val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), indirect, false)
|
||||
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint)
|
||||
builder.setNextUpdate(Date(System.currentTimeMillis() + 1.seconds.toMillis()))
|
||||
serialNumbers.forEach {
|
||||
builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold)
|
||||
}
|
||||
val signer = JcaContentSignerBuilder(signatureAlgorithm).setProvider(Crypto.findProvider("BC")).build(caPrivateKey)
|
||||
return JcaX509CRLConverter().setProvider(Crypto.findProvider("BC")).getCRL(builder.build(signer))
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
@ -455,12 +482,15 @@ class CertificateRevocationListNodeTests {
|
||||
@Path("node.crl")
|
||||
@Produces("application/pkcs7-crl")
|
||||
fun getNodeCRL(): Response {
|
||||
return Response.ok(createRevocationList(
|
||||
return Response.ok(CertificateRevocationListNodeTests.createRevocationList(
|
||||
server,
|
||||
SIGNATURE_ALGORITHM,
|
||||
INTERMEDIATE_CA.certificate,
|
||||
INTERMEDIATE_CA.keyPair.private,
|
||||
NODE_CRL,
|
||||
false,
|
||||
*revokedNodeCerts.toTypedArray()).encoded).build()
|
||||
*revokedNodeCerts.toTypedArray()).encoded)
|
||||
.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -468,11 +498,14 @@ class CertificateRevocationListNodeTests {
|
||||
@Produces("application/pkcs7-crl")
|
||||
fun getIntermediateCRL(): Response {
|
||||
return Response.ok(createRevocationList(
|
||||
server,
|
||||
SIGNATURE_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
INTEMEDIATE_CRL,
|
||||
false,
|
||||
*revokedIntermediateCerts.toTypedArray()).encoded).build()
|
||||
*revokedIntermediateCerts.toTypedArray()).encoded)
|
||||
.build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -480,33 +513,13 @@ class CertificateRevocationListNodeTests {
|
||||
@Produces("application/pkcs7-crl")
|
||||
fun getEmptyCRL(): Response {
|
||||
return Response.ok(createRevocationList(
|
||||
server,
|
||||
SIGNATURE_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
EMPTY_CRL, true).encoded).build()
|
||||
}
|
||||
|
||||
private fun createRevocationList(caCertificate: X509Certificate,
|
||||
caPrivateKey: PrivateKey,
|
||||
endpoint: String,
|
||||
indirect: Boolean,
|
||||
vararg serialNumbers: BigInteger): X509CRL {
|
||||
println("Generating CRL for $endpoint")
|
||||
val builder = JcaX509v2CRLBuilder(caCertificate.subjectX500Principal, Date(System.currentTimeMillis() - 1.minutes.toMillis()))
|
||||
val extensionUtils = JcaX509ExtensionUtils()
|
||||
builder.addExtension(Extension.authorityKeyIdentifier,
|
||||
false, extensionUtils.createAuthorityKeyIdentifier(caCertificate))
|
||||
val issuingDistPointName = GeneralName(
|
||||
GeneralName.uniformResourceIdentifier,
|
||||
"http://${server.hostAndPort.host}:${server.hostAndPort.port}/crl/$endpoint")
|
||||
// This is required and needs to match the certificate settings with respect to being indirect
|
||||
val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), indirect, false)
|
||||
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint)
|
||||
builder.setNextUpdate(Date(System.currentTimeMillis() + 1.seconds.toMillis()))
|
||||
serialNumbers.forEach {
|
||||
builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold)
|
||||
}
|
||||
val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(caPrivateKey)
|
||||
return JcaX509CRLConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCRL(builder.build(signer))
|
||||
EMPTY_CRL,
|
||||
true).encoded)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@ -546,4 +559,32 @@ class CertificateRevocationListNodeTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify CRL algorithms`() {
|
||||
val ECDSA_ALGORITHM = "SHA256withECDSA"
|
||||
val EC_ALGORITHM = "EC"
|
||||
val EMPTY_CRL = "empty.crl"
|
||||
|
||||
val crl = createRevocationList(
|
||||
server,
|
||||
ECDSA_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
EMPTY_CRL,
|
||||
true)
|
||||
// This should pass.
|
||||
crl.verify(ROOT_CA.keyPair.public)
|
||||
|
||||
// Try changing the algorithm to EC will fail.
|
||||
assertFailsWith<IllegalArgumentException>("Unknown signature type requested: EC") {
|
||||
createRevocationList(
|
||||
server,
|
||||
EC_ALGORITHM,
|
||||
ROOT_CA.certificate,
|
||||
ROOT_CA.keyPair.private,
|
||||
EMPTY_CRL,
|
||||
true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
@ -41,7 +42,7 @@ class AttachmentLoadingTests : IntegrationTest() {
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val attachments = MockAttachmentStorage()
|
||||
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments).apply {
|
||||
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply {
|
||||
start(testNetworkParameters().whitelistedContractImplementations)
|
||||
}
|
||||
private val cordapp get() = provider.cordapps.first()
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.node
|
||||
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
|
||||
/**
|
||||
* Encapsulates various pieces of version information of the node.
|
||||
*/
|
||||
@ -17,6 +19,6 @@ data class VersionInfo(
|
||||
val vendor: String) {
|
||||
|
||||
companion object {
|
||||
val UNKNOWN = VersionInfo(1, "Unknown", "Unknown", "Unknown")
|
||||
val UNKNOWN = VersionInfo(PLATFORM_VERSION, "Unknown", "Unknown", "Unknown")
|
||||
}
|
||||
}
|
||||
}
|
@ -56,6 +56,7 @@ import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
|
||||
import net.corda.serialization.internal.*
|
||||
import org.apache.commons.lang.SystemUtils
|
||||
import org.h2.jdbc.JdbcSQLException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -139,6 +140,26 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
// TODO: make this configurable.
|
||||
const val MAX_RPC_MESSAGE_SIZE = 10485760
|
||||
|
||||
fun isValidJavaVersion(): Boolean {
|
||||
if (!hasMinimumJavaVersion()) {
|
||||
println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.")
|
||||
println("Corda will now exit...")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasMinimumJavaVersion(): Boolean {
|
||||
// when the ext.java8_minUpdateVersion gradle constant changes, so must this check
|
||||
val major = SystemUtils.JAVA_VERSION_FLOAT
|
||||
return try {
|
||||
val update = SystemUtils.JAVA_VERSION.substringAfter("_").toLong()
|
||||
major == 1.8F && update >= 171
|
||||
} catch (e: NumberFormatException) { // custom JDKs may not have the update version (e.g. 1.8.0-adoptopenjdk)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val log: Logger get() = staticLog
|
||||
|
@ -16,6 +16,7 @@ import net.corda.core.internal.errors.AddressBindingException
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.*
|
||||
import net.corda.node.internal.Node.Companion.isValidJavaVersion
|
||||
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfigurationImpl
|
||||
@ -36,7 +37,6 @@ import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseMigrationException
|
||||
import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
|
||||
import net.corda.tools.shell.InteractiveShell
|
||||
import org.apache.commons.lang.SystemUtils
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
||||
import picocli.CommandLine.Mixin
|
||||
@ -48,7 +48,6 @@ import java.io.RandomAccessFile
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.net.InetAddress
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.DayOfWeek
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
@ -133,22 +132,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isValidJavaVersion(): Boolean {
|
||||
if (!hasMinimumJavaVersion()) {
|
||||
println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.")
|
||||
println("Corda will now exit...")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasMinimumJavaVersion(): Boolean {
|
||||
// when the ext.java8_minUpdateVersion gradle constant changes, so must this check
|
||||
val major = SystemUtils.JAVA_VERSION_FLOAT
|
||||
val update = SystemUtils.JAVA_VERSION.substringAfter("_").toLong()
|
||||
return major == 1.8F && update >= 171
|
||||
}
|
||||
|
||||
// TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration.
|
||||
// There might be cases where the node user should investigate what went wrong before registering again.
|
||||
private fun checkUnfinishedRegistration() {
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
@ -35,7 +36,7 @@ import kotlin.streams.toList
|
||||
* @property cordappJarPaths The classpath of cordapp JARs
|
||||
*/
|
||||
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
|
||||
versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() {
|
||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() {
|
||||
|
||||
override val cordapps: List<CordappImpl> by lazy { loadCordapps() + coreCordapp }
|
||||
|
||||
@ -104,13 +105,25 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
serializationWhitelists = listOf(),
|
||||
serializationCustomSerializers = listOf(),
|
||||
customSchemas = setOf(),
|
||||
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion),
|
||||
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion),
|
||||
allFlows = listOf(),
|
||||
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
|
||||
jarHash = SecureHash.allOnesHash
|
||||
)
|
||||
|
||||
private fun loadCordapps(): List<CordappImpl> = cordappJarPaths.map { scanCordapp(it).toCordapp(it) }
|
||||
private fun loadCordapps(): List<CordappImpl> {
|
||||
val cordapps = cordappJarPaths.map { scanCordapp(it).toCordapp(it) }
|
||||
.filter {
|
||||
if (it.info.minimumPlatformVersion > versionInfo.platformVersion) {
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) }
|
||||
return cordapps
|
||||
}
|
||||
|
||||
private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl {
|
||||
val info = url.url.openStream().let(::JarInputStream).use { it.manifest }.toCordappInfo(CordappImpl.jarName(url.url))
|
||||
|
@ -38,5 +38,8 @@ fun Manifest?.toCordappInfo(defaultShortName: String): CordappImpl.Info {
|
||||
this?.mainAttributes?.getValue("Implementation-Version")?.let { version ->
|
||||
info = info.copy(version = version)
|
||||
}
|
||||
val minPlatformVersion = this?.mainAttributes?.getValue("Min-Platform-Version")?.toInt() ?: 1
|
||||
val targetPlatformVersion = this?.mainAttributes?.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion
|
||||
info = info.copy(minimumPlatformVersion = minPlatformVersion, targetPlatformVersion = targetPlatformVersion)
|
||||
return info
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ package net.corda.node.internal.cordapp
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.internal.MockCordappConfigProvider
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
@ -75,7 +77,7 @@ class CordappProviderImplTests {
|
||||
fun `test cordapp configuration`() {
|
||||
val configProvider = MockCordappConfigProvider()
|
||||
configProvider.cordappConfigs[isolatedCordappName] = validConfig
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
|
||||
val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start(whitelistedContractImplementations) }
|
||||
|
||||
val expected = provider.getAppContext(provider.cordapps.first()).config
|
||||
@ -84,7 +86,7 @@ class CordappProviderImplTests {
|
||||
}
|
||||
|
||||
private fun newCordappProvider(vararg urls: URL): CordappProviderImpl {
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(urls.toList())
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN)
|
||||
return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start(whitelistedContractImplementations) }
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package net.corda.node.internal.cordapp
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.internal.getTimestampAsDirectoryName
|
||||
import net.corda.testing.node.internal.packageInDirectory
|
||||
@ -101,13 +103,70 @@ class JarScanningCordappLoaderTest {
|
||||
@Test
|
||||
fun `cordapp classloader can load cordapp classes`() {
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
|
||||
|
||||
loader.appClassLoader.loadClass(isolatedContractId)
|
||||
loader.appClassLoader.loadClass(isolatedFlowName)
|
||||
}
|
||||
|
||||
private fun cordappLoaderForPackages(packages: Iterable<String>): CordappLoader {
|
||||
@Test
|
||||
fun `cordapp classloader sets target and min version to 1 if not specified`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN)
|
||||
loader.cordapps.filter { !it.info.shortName.equals("corda-core") }.forEach {
|
||||
assertThat(it.info.targetPlatformVersion).isEqualTo(1)
|
||||
assertThat(it.info.minimumPlatformVersion).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() {
|
||||
// load jar with min and target version in manifest
|
||||
// make sure classloader extracts correct values
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN)
|
||||
// exclude the core cordapp
|
||||
val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single()
|
||||
assertThat(cordapp.info.targetPlatformVersion).isEqualTo(3)
|
||||
assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader sets target version to min version if target version is not specified`() {
|
||||
// load jar with minVersion but not targetVersion in manifest
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN)
|
||||
// exclude the core cordapp
|
||||
val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single()
|
||||
assertThat(cordapp.info.targetPlatformVersion).isEqualTo(2)
|
||||
assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader does not load apps when their min platform version is greater than the platform version`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1))
|
||||
// exclude the core cordapp
|
||||
assertThat(loader.cordapps.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader does load apps when their min platform version is less than the platform version`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000))
|
||||
// exclude the core cordapp
|
||||
assertThat(loader.cordapps.size).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2))
|
||||
// exclude the core cordapp
|
||||
assertThat(loader.cordapps.size).isEqualTo(2)
|
||||
}
|
||||
|
||||
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
|
||||
|
||||
val cordapps = cordappsForPackages(packages)
|
||||
return testDirectory().let { directory ->
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -18,6 +18,7 @@ import net.corda.core.node.services.*
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.ServicesForResolutionImpl
|
||||
import net.corda.node.internal.configureDatabase
|
||||
@ -27,6 +28,7 @@ import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.TestIdentity
|
||||
@ -64,10 +66,10 @@ open class MockServices private constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
private fun cordappLoaderForPackages(packages: Iterable<String>): CordappLoader {
|
||||
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
|
||||
|
||||
val cordappPaths = TestCordappDirectories.forPackages(packages)
|
||||
return JarScanningCordappLoader.fromDirectories(cordappPaths)
|
||||
return JarScanningCordappLoader.fromDirectories(cordappPaths, versionInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -276,7 +276,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
}
|
||||
}
|
||||
|
||||
open class MockNode(args: MockNodeArgs, cordappLoader: CordappLoader = JarScanningCordappLoader.fromDirectories(args.config.cordappDirectories)) : AbstractNode<TestStartedNode>(
|
||||
open class MockNode(args: MockNodeArgs, cordappLoader: CordappLoader = JarScanningCordappLoader.fromDirectories(args.config.cordappDirectories, args.version)) : AbstractNode<TestStartedNode>(
|
||||
args.config,
|
||||
TestClock(Clock.systemUTC()),
|
||||
DefaultNamedCacheFactory(),
|
||||
@ -475,7 +475,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
val cordappDirectories = sharedCorDappsDirectories + TestCordappDirectories.cached(cordapps)
|
||||
doReturn(cordappDirectories).whenever(config).cordappDirectories
|
||||
|
||||
val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version), JarScanningCordappLoader.fromDirectories(cordappDirectories))
|
||||
val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version), JarScanningCordappLoader.fromDirectories(cordappDirectories, parameters.version))
|
||||
_nodes += node
|
||||
if (start) {
|
||||
node.start()
|
||||
|
@ -21,6 +21,7 @@ import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.testThreadFactory
|
||||
import net.corda.testing.node.User
|
||||
import org.apache.commons.lang.SystemUtils
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -158,5 +159,10 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
|
||||
|
||||
class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo) : EnterpriseNode(configuration, versionInfo, false) {
|
||||
|
||||
override fun start() : NodeInfo {
|
||||
check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." }
|
||||
return super.start()
|
||||
}
|
||||
|
||||
override val rxIoScheduler get() = CachedThreadScheduler(testThreadFactory()).also { runOnStop += it::shutdown }
|
||||
}
|
||||
|
@ -5,10 +5,7 @@ package net.corda.testing.core
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
@ -25,7 +22,6 @@ import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
@ -46,7 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
* - The Int.DOLLARS syntax doesn't work from Java. Use the DOLLARS(int) function instead.
|
||||
*/
|
||||
|
||||
/** Returns a fake state reference for testing purposes **/
|
||||
/** Returns a fake state reference for testing purposes. **/
|
||||
fun generateStateRef(): StateRef = StateRef(SecureHash.randomSHA256(), 0)
|
||||
|
||||
private val freePortCounter = AtomicInteger(30000)
|
||||
@ -112,7 +108,7 @@ fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): Party
|
||||
|
||||
private val count = AtomicInteger(0)
|
||||
/**
|
||||
* Randomise a party name to avoid clashes with other tests
|
||||
* Randomise a party name to avoid clashes with other tests.
|
||||
*/
|
||||
fun makeUnique(name: CordaX500Name) = name.copy(commonName =
|
||||
if (name.commonName == null) {
|
||||
@ -131,24 +127,28 @@ class TestIdentity(val name: CordaX500Name, val keyPair: KeyPair) {
|
||||
* Creates an identity that won't equal any other. This is mostly useful as a throwaway for test helpers.
|
||||
* @param organisation the organisation part of the new identity's name.
|
||||
*/
|
||||
fun fresh(organisation: String): TestIdentity {
|
||||
val keyPair = generateKeyPair()
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun fresh(organisation: String, signatureScheme: SignatureScheme = Crypto.DEFAULT_SIGNATURE_SCHEME): TestIdentity {
|
||||
val keyPair = Crypto.generateKeyPair(signatureScheme)
|
||||
val name = CordaX500Name(organisation, keyPair.public.toStringShort(), CordaX500Name.unspecifiedCountry)
|
||||
return TestIdentity(name, keyPair)
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates an identity with a deterministic [keyPair] i.e. same [entropy] same keyPair .*/
|
||||
constructor(name: CordaX500Name, entropy: Long) : this(name, entropyToKeyPair(BigInteger.valueOf(entropy)))
|
||||
/** Creates an identity with a deterministic [keyPair] i.e. same [entropy] same keyPair. */
|
||||
@JvmOverloads constructor(name: CordaX500Name, entropy: Long, signatureScheme: SignatureScheme = Crypto.DEFAULT_SIGNATURE_SCHEME)
|
||||
: this(name, Crypto.deriveKeyPairFromEntropy(signatureScheme, BigInteger.valueOf(entropy)))
|
||||
|
||||
/** Creates an identity with the given name and a fresh keyPair. */
|
||||
constructor(name: CordaX500Name) : this(name, generateKeyPair())
|
||||
@JvmOverloads constructor(name: CordaX500Name, signatureScheme: SignatureScheme = Crypto.DEFAULT_SIGNATURE_SCHEME)
|
||||
: this(name, Crypto.generateKeyPair(signatureScheme))
|
||||
|
||||
val publicKey: PublicKey get() = keyPair.public
|
||||
val party: Party = Party(name, publicKey)
|
||||
val identity: PartyAndCertificate by lazy { getTestPartyAndCertificate(party) } // Often not needed.
|
||||
|
||||
/** Returns a [PartyAndReference] for this identity and the given reference */
|
||||
/** Returns a [PartyAndReference] for this identity and the given reference. */
|
||||
fun ref(vararg bytes: Byte): PartyAndReference = party.ref(*bytes)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user