mirror of
https://github.com/corda/corda.git
synced 2025-05-09 12:02:56 +00:00
Deterministic JVM (#3386)
* CID-251 - Deterministic JVM * CID-251 - Add DJVM documentation * CID-251 - Address review comments from @chrisr3 * CID-251 - Address further review comments from @chrisr3 * CID-251 - Use shadowJar to generate fat JAR * CID-251 - Address review comments from @exFalso * CID-251 - Improve naming in ReferenceMap * CID-251 - Add test for Kotlin meta-class behaviour * CID-251 - Address review comments from @shamsasari * CID-251 - Add description of high-level flow * CID-251 - Refactoring * CID-251 - Rename package to net.corda.djvm * CID-251 - Include deterministic-rt.jar as runtime dependency * CID-251 - Add Gradle task for generating whitelist from deterministic rt.jar * CID-251 - Error messages for StackOverflow/OutOfMemory, update whitelist * CID-251 - Reduce set definition of pinned classes * CID-251 - Tidy up logic around pinned classes * CID-251 - Shade ASM dependency and split out CLI tool * CID-251 - Address review comments from @mikehearn (part 1) * CID-251 - Address review comments from @mikehearn (part 2) * CID-251 - Address review comments from @mikehearn (part 3) * CID-251 - Address review comments from @exFalso * CID-251 - Address review comments from @mikehearn (part 4) * CID-251 - Address review comments from @exFalso and @mikehearn * CID-251 - Address review comments from @mikehearn (part 5)
This commit is contained in:
parent
30d07bc998
commit
d2ef16cbfd
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -71,6 +71,8 @@
|
|||||||
<module name="data_test" target="1.8" />
|
<module name="data_test" target="1.8" />
|
||||||
<module name="demobench_main" target="1.8" />
|
<module name="demobench_main" target="1.8" />
|
||||||
<module name="demobench_test" target="1.8" />
|
<module name="demobench_test" target="1.8" />
|
||||||
|
<module name="djvm_main" target="1.8" />
|
||||||
|
<module name="djvm_test" target="1.8" />
|
||||||
<module name="docs_main" target="1.8" />
|
<module name="docs_main" target="1.8" />
|
||||||
<module name="docs_source_example-code_integrationTest" target="1.8" />
|
<module name="docs_source_example-code_integrationTest" target="1.8" />
|
||||||
<module name="docs_source_example-code_main" target="1.8" />
|
<module name="docs_source_example-code_main" target="1.8" />
|
||||||
@ -229,4 +231,4 @@
|
|||||||
<component name="JavacSettings">
|
<component name="JavacSettings">
|
||||||
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
|
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
3
djvm/.gitignore
vendored
Normal file
3
djvm/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
tmp/
|
||||||
|
*.log
|
||||||
|
*.log.gz
|
89
djvm/build.gradle
Normal file
89
djvm/build.gradle
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
buildscript {
|
||||||
|
// Shaded version of ASM to avoid conflict with root project.
|
||||||
|
ext.asm_version = '6.1.1'
|
||||||
|
ext.deterministic_rt_version = '1.0-20180625.120901-7'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'com.github.johnrengelman.shadow' version '2.0.3'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
|
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||||
|
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||||
|
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
|
||||||
|
|
||||||
|
// ASM: byte code manipulation library
|
||||||
|
compile "org.ow2.asm:asm:$asm_version"
|
||||||
|
compile "org.ow2.asm:asm-tree:$asm_version"
|
||||||
|
compile "org.ow2.asm:asm-commons:$asm_version"
|
||||||
|
|
||||||
|
// Classpath scanner
|
||||||
|
compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version"
|
||||||
|
|
||||||
|
// Deterministic runtime - used in whitelist generation
|
||||||
|
runtime "net.corda:deterministic-rt:$deterministic_rt_version:api"
|
||||||
|
|
||||||
|
// Test utilities
|
||||||
|
testCompile "junit:junit:$junit_version"
|
||||||
|
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url "$artifactory_contextUrl/corda-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task generateWhitelist(type: JavaExec) {
|
||||||
|
// This is an example of how a whitelist can be generated from a JAR. In most applications though, it is recommended
|
||||||
|
// thet the minimal set whitelist is used.
|
||||||
|
def jars = configurations.runtime.collect {
|
||||||
|
it.toString()
|
||||||
|
}.findAll {
|
||||||
|
it.toString().contains("deterministic-rt")
|
||||||
|
}
|
||||||
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
|
main = 'net.corda.djvm.tools.cli.Program'
|
||||||
|
args = ['whitelist', 'generate', '-o', 'src/main/resources/jdk8-deterministic.dat.gz'] + jars
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Automatic-Module-Name': 'net.corda.djvm'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shadowJar {
|
||||||
|
baseName = "djvm"
|
||||||
|
classifier = ""
|
||||||
|
exclude 'deterministic-rt*.jar'
|
||||||
|
dependencies {
|
||||||
|
exclude(dependency('com.jcabi:.*:.*'))
|
||||||
|
exclude(dependency('org.apache.*:.*:.*'))
|
||||||
|
exclude(dependency('org.jetbrains.*:.*:.*'))
|
||||||
|
exclude(dependency('org.slf4j:.*:.*'))
|
||||||
|
exclude(dependency('io.github.lukehutch:.*:.*'))
|
||||||
|
}
|
||||||
|
relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm'
|
||||||
|
artifacts {
|
||||||
|
shadow(tasks.shadowJar.archivePath) {
|
||||||
|
builtBy shadowJar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
djvm/cli/build.gradle
Normal file
45
djvm/cli/build.gradle
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'com.github.johnrengelman.shadow'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
|
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||||
|
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||||
|
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
|
||||||
|
|
||||||
|
compile "info.picocli:picocli:$picocli_version"
|
||||||
|
compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version"
|
||||||
|
compile project(path: ":djvm", configuration: "shadow")
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Main-Class': 'net.corda.djvm.tools.cli.Program',
|
||||||
|
'Automatic-Module-Name': 'net.corda.djvm',
|
||||||
|
'Build-Date': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shadowJar {
|
||||||
|
baseName = "corda-djvm"
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.tools.Utilities.createCodePath
|
||||||
|
import net.corda.djvm.tools.Utilities.getFileNames
|
||||||
|
import net.corda.djvm.tools.Utilities.jarPath
|
||||||
|
import picocli.CommandLine.Command
|
||||||
|
import picocli.CommandLine.Parameters
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "build",
|
||||||
|
description = ["Build one or more Java source files, each implementing the sandbox runnable interface " +
|
||||||
|
"required for execution in the deterministic sandbox."]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class BuildCommand : CommandBase() {
|
||||||
|
|
||||||
|
@Parameters
|
||||||
|
var files: Array<Path> = emptyArray()
|
||||||
|
|
||||||
|
override fun validateArguments() = files.isNotEmpty()
|
||||||
|
|
||||||
|
override fun handleCommand(): Boolean {
|
||||||
|
val codePath = createCodePath()
|
||||||
|
val files = files.getFileNames { codePath.resolve(it) }
|
||||||
|
printVerbose("Compiling ${files.joinToString(", ")}...")
|
||||||
|
ProcessBuilder("javac", "-cp", "tmp:$jarPath", *files).apply {
|
||||||
|
inheritIO()
|
||||||
|
environment().putAll(System.getenv())
|
||||||
|
start().apply {
|
||||||
|
waitFor()
|
||||||
|
return (exitValue() == 0).apply {
|
||||||
|
if (this) {
|
||||||
|
printInfo("Build succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.source.ClassSource
|
||||||
|
import picocli.CommandLine.Command
|
||||||
|
import picocli.CommandLine.Parameters
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "check",
|
||||||
|
description = ["Statically validate that a class or set of classes (and their dependencies) do not violate any " +
|
||||||
|
"constraints posed by the deterministic sandbox environment."]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class CheckCommand : ClassCommand() {
|
||||||
|
|
||||||
|
override val filters: Array<String>
|
||||||
|
get() = classes
|
||||||
|
|
||||||
|
@Parameters(description = ["The partial or fully qualified names of the Java classes to analyse and validate."])
|
||||||
|
var classes: Array<String> = emptyArray()
|
||||||
|
|
||||||
|
override fun printSuccess(classes: List<Class<*>>) {
|
||||||
|
for (clazz in classes.sortedBy { it.name }) {
|
||||||
|
printVerbose("Class ${clazz.name} validated")
|
||||||
|
}
|
||||||
|
printVerbose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processClasses(classes: List<Class<*>>) {
|
||||||
|
val sources = classes.map { ClassSource.fromClassName(it.name) }
|
||||||
|
val summary = executor.validate(*sources.toTypedArray())
|
||||||
|
printMessages(summary.messages, summary.classOrigins)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,216 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.SandboxConfiguration
|
||||||
|
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||||
|
import net.corda.djvm.analysis.Whitelist
|
||||||
|
import net.corda.djvm.execution.*
|
||||||
|
import net.corda.djvm.references.ClassModule
|
||||||
|
import net.corda.djvm.source.ClassSource
|
||||||
|
import net.corda.djvm.source.SourceClassLoader
|
||||||
|
import net.corda.djvm.tools.Utilities.find
|
||||||
|
import net.corda.djvm.tools.Utilities.onEmpty
|
||||||
|
import net.corda.djvm.tools.Utilities.userClassPath
|
||||||
|
import net.corda.djvm.utilities.Discovery
|
||||||
|
import djvm.org.objectweb.asm.ClassReader
|
||||||
|
import picocli.CommandLine.Option
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
@Suppress("KDocMissingDocumentation", "MemberVisibilityCanBePrivate")
|
||||||
|
abstract class ClassCommand : CommandBase() {
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-p", "--profile"],
|
||||||
|
description = ["The execution profile to use (DEFAULT, UNLIMITED, DISABLE_BRANCHING or DISABLE_THROWS)."]
|
||||||
|
)
|
||||||
|
var profile: ExecutionProfile = ExecutionProfile.DEFAULT
|
||||||
|
|
||||||
|
@Option(names = ["--ignore-rules"], description = ["Disable all rules pertaining to the sandbox."])
|
||||||
|
var ignoreRules: Boolean = false
|
||||||
|
|
||||||
|
@Option(names = ["--ignore-emitters"], description = ["Disable all emitters defined for the sandbox."])
|
||||||
|
var ignoreEmitters: Boolean = false
|
||||||
|
|
||||||
|
@Option(names = ["--ignore-definition-providers"], description = ["Disable all definition providers."])
|
||||||
|
var ignoreDefinitionProviders: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-w", "--whitelist"],
|
||||||
|
description = ["Override the default whitelist. Use provided whitelist instead. If NONE is provided, the " +
|
||||||
|
"whitelist will be ignored. If ALL is provided, all references will be whitelisted. LANG can be " +
|
||||||
|
"used to only whitelist select classes and their members from the java.lang package."]
|
||||||
|
)
|
||||||
|
var whitelist: Path? = null
|
||||||
|
|
||||||
|
@Option(names = ["-c", "--classpath"], description = ["Additions to the default class path."], split = ":")
|
||||||
|
var classPath: Array<Path> = emptyArray()
|
||||||
|
|
||||||
|
@Option(names = ["--disable-tracing"], description = ["Disable tracing in the sandbox."])
|
||||||
|
var disableTracing: Boolean = false
|
||||||
|
|
||||||
|
@Option(names = ["--analyze-annotations"], description = ["Analyze all annotations even if they are not " +
|
||||||
|
"explicitly referenced."])
|
||||||
|
var analyzeAnnotations: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["--prefix-filters"],
|
||||||
|
description = ["Only record messages matching one of the provided prefixes."],
|
||||||
|
split = ":"
|
||||||
|
)
|
||||||
|
var prefixFilters: Array<String> = emptyArray()
|
||||||
|
|
||||||
|
abstract val filters: Array<String>
|
||||||
|
|
||||||
|
private val classModule = ClassModule()
|
||||||
|
|
||||||
|
private lateinit var classLoader: ClassLoader
|
||||||
|
|
||||||
|
protected var executor = SandboxExecutor<Any, Any>()
|
||||||
|
|
||||||
|
private var derivedWhitelist: Whitelist = Whitelist.MINIMAL
|
||||||
|
|
||||||
|
abstract fun processClasses(classes: List<Class<*>>)
|
||||||
|
|
||||||
|
open fun printSuccess(classes: List<Class<*>>) {}
|
||||||
|
|
||||||
|
override fun validateArguments() = filters.isNotEmpty()
|
||||||
|
|
||||||
|
override fun handleCommand(): Boolean {
|
||||||
|
derivedWhitelist = whitelistFromPath(whitelist)
|
||||||
|
val configuration = getConfiguration(derivedWhitelist)
|
||||||
|
classLoader = SourceClassLoader(getClasspath(), configuration.analysisConfiguration.classResolver)
|
||||||
|
createExecutor(configuration)
|
||||||
|
|
||||||
|
val classes = discoverClasses(filters).onEmpty {
|
||||||
|
throw Exception("Could not find any classes matching ${filters.joinToString(" ")} on the " +
|
||||||
|
"system class path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
processClasses(classes)
|
||||||
|
printSuccess(classes)
|
||||||
|
true
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
printException(exception)
|
||||||
|
if (exception is SandboxException) {
|
||||||
|
printCosts(exception.executionSummary.costs)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun printCosts(costs: CostSummary) {
|
||||||
|
if (disableTracing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printInfo("Runtime Cost Summary:")
|
||||||
|
printInfo(" - allocations = @|yellow ${costs.allocations}|@")
|
||||||
|
printInfo(" - invocations = @|yellow ${costs.invocations}|@")
|
||||||
|
printInfo(" - jumps = @|yellow ${costs.jumps}|@")
|
||||||
|
printInfo(" - throws = @|yellow ${costs.throws}|@")
|
||||||
|
printInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun discoverClasses(filters: Array<String>): List<Class<*>> {
|
||||||
|
return findDiscoverableRunnables(filters) + findReferencedClasses(filters) + findClassesInJars(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findDiscoverableRunnables(filters: Array<String>): List<Class<*>> {
|
||||||
|
val classes = find<DiscoverableRunnable>()
|
||||||
|
val applicableFilters = filters
|
||||||
|
.filter { !isJarFile(it) && !isFullClassName(it) }
|
||||||
|
val filteredClasses = applicableFilters
|
||||||
|
.flatMap { filter ->
|
||||||
|
classes.filter { clazz ->
|
||||||
|
clazz.name.contains(filter, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicableFilters.isNotEmpty() && filteredClasses.isEmpty()) {
|
||||||
|
throw Exception("Could not find any classes implementing ${SandboxedRunnable::class.java.simpleName} " +
|
||||||
|
"whose name matches '${applicableFilters.joinToString(" ")}'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicableFilters.isNotEmpty()) {
|
||||||
|
printVerbose("Class path: $userClassPath")
|
||||||
|
printVerbose("Discovered runnables on the class path:")
|
||||||
|
for (clazz in classes) {
|
||||||
|
printVerbose(" - ${clazz.name}")
|
||||||
|
}
|
||||||
|
printVerbose()
|
||||||
|
}
|
||||||
|
return filteredClasses
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findReferencedClasses(filters: Array<String>): List<Class<*>> {
|
||||||
|
return filters.filter { !isJarFile(it) && isFullClassName(it) }.map {
|
||||||
|
val className = classModule.getFormattedClassName(it)
|
||||||
|
printVerbose("Looking up class $className...")
|
||||||
|
lookUpClass(className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findClassesInJars(filters: Array<String>): List<Class<*>> {
|
||||||
|
return filters.filter { isJarFile(it) }.flatMap { jarFile ->
|
||||||
|
mutableListOf<Class<*>>().apply {
|
||||||
|
ClassSource.fromPath(Paths.get(jarFile)).getStreamIterator().forEach {
|
||||||
|
val reader = ClassReader(it)
|
||||||
|
val className = classModule.getFormattedClassName(reader.className)
|
||||||
|
printVerbose("Looking up class $className in $jarFile...")
|
||||||
|
this.add(lookUpClass(className))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lookUpClass(className: String): Class<*> {
|
||||||
|
return try {
|
||||||
|
classLoader.loadClass(className)
|
||||||
|
} catch (exception: NoClassDefFoundError) {
|
||||||
|
val reference = exception.message?.let {
|
||||||
|
"referenced class ${classModule.getFormattedClassName(it)} in "
|
||||||
|
} ?: ""
|
||||||
|
throw Exception("Unable to load ${reference}type $className (is it present on the class path?)")
|
||||||
|
} catch (exception: TypeNotPresentException) {
|
||||||
|
val reference = exception.typeName() ?: ""
|
||||||
|
throw Exception("Type $reference not present in class $className")
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
throw Exception("Unable to load type $className (is it present on the class path?)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isJarFile(filter: String) = Files.exists(Paths.get(filter)) && filter.endsWith(".jar", true)
|
||||||
|
|
||||||
|
private fun isFullClassName(filter: String) = filter.count { it == '.' } > 0
|
||||||
|
|
||||||
|
private fun getClasspath() =
|
||||||
|
classPath.toList() + filters.filter { it.endsWith(".jar", true) }.map { Paths.get(it) }
|
||||||
|
|
||||||
|
private fun getConfiguration(whitelist: Whitelist): SandboxConfiguration {
|
||||||
|
return SandboxConfiguration.of(
|
||||||
|
profile = profile,
|
||||||
|
rules = if (ignoreRules) { emptyList() } else { Discovery.find() },
|
||||||
|
emitters = ignoreEmitters.emptyListIfTrueOtherwiseNull(),
|
||||||
|
definitionProviders = if(ignoreDefinitionProviders) { emptyList() } else { Discovery.find() },
|
||||||
|
enableTracing = !disableTracing,
|
||||||
|
analysisConfiguration = AnalysisConfiguration(
|
||||||
|
whitelist = whitelist,
|
||||||
|
minimumSeverityLevel = level,
|
||||||
|
classPath = getClasspath(),
|
||||||
|
analyzeAnnotations = analyzeAnnotations,
|
||||||
|
prefixFilters = prefixFilters.toList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createExecutor(configuration: SandboxConfiguration) {
|
||||||
|
executor = SandboxExecutor(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Boolean.emptyListIfTrueOtherwiseNull(): List<T>? = when (this) {
|
||||||
|
true -> emptyList()
|
||||||
|
false -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
265
djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CommandBase.kt
Normal file
265
djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CommandBase.kt
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.Whitelist
|
||||||
|
import net.corda.djvm.execution.SandboxException
|
||||||
|
import net.corda.djvm.messages.MessageCollection
|
||||||
|
import net.corda.djvm.messages.Severity
|
||||||
|
import net.corda.djvm.references.ClassReference
|
||||||
|
import net.corda.djvm.references.EntityReference
|
||||||
|
import net.corda.djvm.references.MemberReference
|
||||||
|
import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||||
|
import picocli.CommandLine
|
||||||
|
import picocli.CommandLine.Help.Ansi
|
||||||
|
import picocli.CommandLine.Option
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
abstract class CommandBase : Callable<Boolean> {
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-l", "--level"],
|
||||||
|
description = ["The minimum severity level to log (TRACE, INFO, WARNING or ERROR."],
|
||||||
|
converter = [SeverityConverter::class]
|
||||||
|
)
|
||||||
|
protected var level: Severity = Severity.WARNING
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-q", "--quiet"],
|
||||||
|
description = ["Only print important messages to standard output."]
|
||||||
|
)
|
||||||
|
private var quiet: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-v", "--verbose"],
|
||||||
|
description = ["Enable verbose logging."]
|
||||||
|
)
|
||||||
|
private var verbose: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["--debug"],
|
||||||
|
description = ["Print full stack traces upon error."]
|
||||||
|
)
|
||||||
|
private var debug: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["--colors"],
|
||||||
|
description = ["Use colors when printing to terminal."]
|
||||||
|
)
|
||||||
|
private var useColors: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["--no-colors"],
|
||||||
|
description = ["Do not use colors when printing to terminal."]
|
||||||
|
)
|
||||||
|
private var useNoColors: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["--compact"],
|
||||||
|
description = ["Print compact errors and warnings."]
|
||||||
|
)
|
||||||
|
private var compact: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["--print-origins"],
|
||||||
|
description = ["Print origins for errors and warnings."]
|
||||||
|
)
|
||||||
|
private var printOrigins: Boolean = false
|
||||||
|
|
||||||
|
private val ansi: Ansi
|
||||||
|
get() = when {
|
||||||
|
useNoColors -> Ansi.OFF
|
||||||
|
useColors -> Ansi.ON
|
||||||
|
else -> Ansi.AUTO
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SeverityConverter : CommandLine.ITypeConverter<Severity> {
|
||||||
|
override fun convert(value: String): Severity {
|
||||||
|
return try {
|
||||||
|
when (value.toUpperCase()) {
|
||||||
|
"INFO" -> Severity.INFORMATIONAL
|
||||||
|
else -> Severity.valueOf(value.toUpperCase())
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
val candidates = Severity.values().filter { it.name.startsWith(value, true) }
|
||||||
|
if (candidates.size == 1) {
|
||||||
|
candidates.first()
|
||||||
|
} else {
|
||||||
|
println("ERROR: Must be one of ${Severity.values().joinToString(", ") { it.name }}")
|
||||||
|
Severity.INFORMATIONAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun call(): Boolean {
|
||||||
|
if (!validateArguments()) {
|
||||||
|
CommandLine.usage(this, System.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (verbose && quiet) {
|
||||||
|
printError("Error: Cannot set verbose and quiet modes at the same time")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
handleCommand()
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
printException(exception)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun printException(exception: Throwable) = when (exception) {
|
||||||
|
is SandboxClassLoadingException -> {
|
||||||
|
printMessages(exception.messages, exception.classOrigins)
|
||||||
|
printError()
|
||||||
|
}
|
||||||
|
is SandboxException -> {
|
||||||
|
val cause = exception.cause
|
||||||
|
when (cause) {
|
||||||
|
is SandboxClassLoadingException -> {
|
||||||
|
printMessages(cause.messages, cause.classOrigins)
|
||||||
|
printError()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (debug) {
|
||||||
|
exception.exception.printStackTrace(System.err)
|
||||||
|
} else {
|
||||||
|
printError("Error: ${errorMessage(exception.exception)}")
|
||||||
|
}
|
||||||
|
printError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (debug) {
|
||||||
|
exception.printStackTrace(System.err)
|
||||||
|
} else {
|
||||||
|
printError("Error: ${errorMessage(exception)}")
|
||||||
|
printError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun errorMessage(exception: Throwable): String {
|
||||||
|
return when (exception) {
|
||||||
|
is StackOverflowError -> "Stack overflow"
|
||||||
|
is OutOfMemoryError -> "Out of memory"
|
||||||
|
is ThreadDeath -> "Thread death"
|
||||||
|
else -> {
|
||||||
|
val message = exception.message
|
||||||
|
when {
|
||||||
|
message.isNullOrBlank() -> exception.javaClass.simpleName
|
||||||
|
else -> message!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun printMessages(messages: MessageCollection, origins: Map<String, Set<EntityReference>> = emptyMap()) {
|
||||||
|
val sortedMessages = messages.sorted()
|
||||||
|
val errorCount = messages.errorCount.countOf("error")
|
||||||
|
val warningCount = messages.warningCount.countOf("warning")
|
||||||
|
printInfo("Found $errorCount and $warningCount")
|
||||||
|
if (!compact) {
|
||||||
|
printInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = true
|
||||||
|
for (message in sortedMessages) {
|
||||||
|
val severityColor = message.severity.color ?: "blue"
|
||||||
|
val location = message.location.format().let {
|
||||||
|
when {
|
||||||
|
it.isNotBlank() -> "in $it: "
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (compact) {
|
||||||
|
printError(" - @|$severityColor ${message.severity}|@ $location${message.message}.")
|
||||||
|
} else {
|
||||||
|
if (!first) {
|
||||||
|
printError()
|
||||||
|
}
|
||||||
|
printError(" - @|$severityColor ${message.severity}|@ $location\n ${message.message}.")
|
||||||
|
}
|
||||||
|
if (printOrigins) {
|
||||||
|
val classOrigins = origins[message.location.className.replace("/", ".")] ?: emptySet()
|
||||||
|
for (classOrigin in classOrigins.groupBy({ it.className }, { it })) {
|
||||||
|
val count = classOrigin.value.count()
|
||||||
|
val reference = when (count) {
|
||||||
|
1 -> classOrigin.value.first()
|
||||||
|
else -> ClassReference(classOrigin.value.first().className)
|
||||||
|
}
|
||||||
|
when (reference) {
|
||||||
|
is ClassReference ->
|
||||||
|
printError(" - Reference from ${reference.className}")
|
||||||
|
is MemberReference ->
|
||||||
|
printError(" - Reference from ${reference.className}.${reference.memberName}()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printError()
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun handleCommand(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun validateArguments(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun printInfo(message: String = "") {
|
||||||
|
if (!quiet) {
|
||||||
|
println(ansi.Text(message).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun printVerbose(message: String = "") {
|
||||||
|
if (verbose) {
|
||||||
|
println(ansi.Text(message).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun printError(message: String = "") {
|
||||||
|
System.err.println(ansi.Text(message).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun printResult(result: Any?) {
|
||||||
|
printInfo("Execution successful")
|
||||||
|
printInfo(" - result = $result")
|
||||||
|
printInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun whitelistFromPath(whitelist: Path?): Whitelist {
|
||||||
|
return whitelist?.let {
|
||||||
|
if ("$it" == "NONE") {
|
||||||
|
Whitelist.EMPTY
|
||||||
|
} else if ("$it" == "ALL") {
|
||||||
|
Whitelist.EVERYTHING
|
||||||
|
} else if ("$it" == "LANG") {
|
||||||
|
Whitelist.MINIMAL
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Whitelist.fromFile(file = it)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
throw Exception("Failed to load whitelist '$it'", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: Whitelist.MINIMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.countOf(suffix: String): String {
|
||||||
|
return this.let {
|
||||||
|
when (it) {
|
||||||
|
0 -> "no ${suffix}s"
|
||||||
|
1 -> "@|yellow 1|@ $suffix"
|
||||||
|
else -> "@|yellow $it|@ ${suffix}s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import picocli.CommandLine
|
||||||
|
import picocli.CommandLine.Command
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "djvm",
|
||||||
|
versionProvider = VersionProvider::class,
|
||||||
|
description = ["JVM for running programs in a deterministic sandbox."],
|
||||||
|
mixinStandardHelpOptions = true,
|
||||||
|
subcommands = [
|
||||||
|
BuildCommand::class,
|
||||||
|
CheckCommand::class,
|
||||||
|
InspectionCommand::class,
|
||||||
|
NewCommand::class,
|
||||||
|
RunCommand::class,
|
||||||
|
ShowCommand::class,
|
||||||
|
TreeCommand::class,
|
||||||
|
WhitelistCommand::class
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class Commands : CommandBase() {
|
||||||
|
|
||||||
|
fun run(args: Array<String>) = when (CommandLine.call(this, System.err, *args)) {
|
||||||
|
true -> 0
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.source.ClassSource
|
||||||
|
import net.corda.djvm.tools.Utilities.createCodePath
|
||||||
|
import picocli.CommandLine.Command
|
||||||
|
import picocli.CommandLine.Parameters
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "inspect",
|
||||||
|
description = ["Inspect the transformations that are being applied to classes before they get loaded into " +
|
||||||
|
"the sandbox."]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class InspectionCommand : ClassCommand() {
|
||||||
|
|
||||||
|
override val filters: Array<String>
|
||||||
|
get() = classes
|
||||||
|
|
||||||
|
@Parameters(description = ["The partial or fully qualified names of the Java classes to inspect."])
|
||||||
|
var classes: Array<String> = emptyArray()
|
||||||
|
|
||||||
|
override fun processClasses(classes: List<Class<*>>) {
|
||||||
|
val sources = classes.map { ClassSource.fromClassName(it.name) }
|
||||||
|
val (_, messages) = executor.validate(*sources.toTypedArray())
|
||||||
|
|
||||||
|
if (messages.isNotEmpty()) {
|
||||||
|
for (message in messages.sorted()) {
|
||||||
|
printInfo(" - $message")
|
||||||
|
}
|
||||||
|
printInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (classSource in sources) {
|
||||||
|
val loadedClass = executor.load(classSource)
|
||||||
|
val sourceClass = createCodePath().resolve("${loadedClass.type.simpleName}.class")
|
||||||
|
val originalClass = Files.createTempFile("sandbox-", ".java")
|
||||||
|
val transformedClass = Files.createTempFile("sandbox-", ".java")
|
||||||
|
|
||||||
|
printInfo("Class: ${loadedClass.name}")
|
||||||
|
printVerbose(" - Size of the original byte code: ${Files.size(sourceClass)}")
|
||||||
|
printVerbose(" - Size of the transformed byte code: ${loadedClass.byteCode.bytes.size}")
|
||||||
|
printVerbose(" - Original class: $originalClass")
|
||||||
|
printVerbose(" - Transformed class: $transformedClass")
|
||||||
|
printInfo()
|
||||||
|
|
||||||
|
// Generate byte code dump of the original class
|
||||||
|
ProcessBuilder("javap", "-c", sourceClass.toString()).apply {
|
||||||
|
redirectOutput(originalClass.toFile())
|
||||||
|
environment().putAll(System.getenv())
|
||||||
|
start().apply {
|
||||||
|
waitFor()
|
||||||
|
exitValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate byte code dump of the transformed class
|
||||||
|
Files.createTempFile("sandbox-", ".class").apply {
|
||||||
|
Files.write(this, loadedClass.byteCode.bytes)
|
||||||
|
ProcessBuilder("javap", "-c", this.toString()).apply {
|
||||||
|
redirectOutput(transformedClass.toFile())
|
||||||
|
environment().putAll(System.getenv())
|
||||||
|
start().apply {
|
||||||
|
waitFor()
|
||||||
|
exitValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Files.delete(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and display the difference between the original and the transformed class
|
||||||
|
ProcessBuilder(
|
||||||
|
"git", "diff", originalClass.toString(), transformedClass.toString()
|
||||||
|
).apply {
|
||||||
|
inheritIO()
|
||||||
|
environment().putAll(System.getenv())
|
||||||
|
start().apply {
|
||||||
|
waitFor()
|
||||||
|
exitValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printInfo()
|
||||||
|
|
||||||
|
Files.deleteIfExists(originalClass)
|
||||||
|
Files.deleteIfExists(transformedClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.tools.Utilities.baseName
|
||||||
|
import net.corda.djvm.tools.Utilities.createCodePath
|
||||||
|
import net.corda.djvm.tools.Utilities.getFiles
|
||||||
|
import net.corda.djvm.tools.Utilities.openOptions
|
||||||
|
import picocli.CommandLine.*
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "new",
|
||||||
|
description = ["Create one or more new Java classes implementing the sandbox runnable interface that is " +
|
||||||
|
"required for execution in the deterministic sandbox. Each Java file is created using a template, " +
|
||||||
|
"with class name derived from the provided file name."
|
||||||
|
],
|
||||||
|
showDefaultValues = true
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class NewCommand : CommandBase() {
|
||||||
|
|
||||||
|
@Parameters(description = ["The names of the Java source files that will be created."])
|
||||||
|
var files: Array<Path> = emptyArray()
|
||||||
|
|
||||||
|
@Option(names = ["-f", "--force"], description = ["Forcefully overwrite files if they already exist."])
|
||||||
|
var force: Boolean = false
|
||||||
|
|
||||||
|
@Option(names = ["--from"], description = ["The input type to use for the constructed runnable."])
|
||||||
|
var fromType: String = "Object"
|
||||||
|
|
||||||
|
@Option(names = ["--to"], description = ["The output type to use for the constructed runnable."])
|
||||||
|
var toType: String = "Object"
|
||||||
|
|
||||||
|
@Option(names = ["--return"], description = ["The default return value for the constructed runnable."])
|
||||||
|
var returnValue: String = "null"
|
||||||
|
|
||||||
|
override fun validateArguments() = files.isNotEmpty()
|
||||||
|
|
||||||
|
override fun handleCommand(): Boolean {
|
||||||
|
val codePath = createCodePath()
|
||||||
|
val files = files.getFiles { codePath.resolve(it) }
|
||||||
|
for (file in files) {
|
||||||
|
try {
|
||||||
|
printVerbose("Creating file '$file'...")
|
||||||
|
Files.newBufferedWriter(file, *openOptions(force)).use {
|
||||||
|
it.append(TEMPLATE
|
||||||
|
.replace("[NAME]", file.baseName)
|
||||||
|
.replace("[FROM]", fromType)
|
||||||
|
.replace("[TO]", toType)
|
||||||
|
.replace("[RETURN]", returnValue))
|
||||||
|
}
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
throw Exception("Failed to create file '$file'", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val TEMPLATE = """
|
||||||
|
|package net.corda.djvm;
|
||||||
|
|
|
||||||
|
|import net.corda.djvm.execution.SandboxedRunnable;
|
||||||
|
|
|
||||||
|
|public class [NAME] implements SandboxedRunnable<[FROM], [TO]> {
|
||||||
|
| @Override
|
||||||
|
| public [TO] run([FROM] input) {
|
||||||
|
| return [RETURN];
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Program.kt
Normal file
12
djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Program.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@file:JvmName("Program")
|
||||||
|
|
||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entry point of the deterministic sandbox tool.
|
||||||
|
*/
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
exitProcess(Commands().run(args))
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "run",
|
||||||
|
description = ["Execute runnable in sandbox."],
|
||||||
|
showDefaultValues = true
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class RunCommand : ClassCommand() {
|
||||||
|
|
||||||
|
override val filters: Array<String>
|
||||||
|
get() = classes
|
||||||
|
|
||||||
|
@Parameters(description = ["The partial or fully qualified names of the Java classes to run."])
|
||||||
|
var classes: Array<String> = emptyArray()
|
||||||
|
|
||||||
|
override fun processClasses(classes: List<Class<*>>) {
|
||||||
|
val interfaceName = SandboxedRunnable::class.java.simpleName
|
||||||
|
for (clazz in classes) {
|
||||||
|
if (!clazz.interfaces.any { it.simpleName == interfaceName }) {
|
||||||
|
printError("Class is not an instance of $interfaceName; ${clazz.name}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printInfo("Running class ${clazz.name}...")
|
||||||
|
executor.run(ClassSource.fromClassName(clazz.name), Any()).apply {
|
||||||
|
printResult(result)
|
||||||
|
printCosts(costs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.source.ClassSource
|
||||||
|
import picocli.CommandLine.Command
|
||||||
|
import picocli.CommandLine.Parameters
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "show",
|
||||||
|
description = ["Show the transformed version of a class as it is prepared for execution in the deterministic " +
|
||||||
|
"sandbox."]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class ShowCommand : ClassCommand() {
|
||||||
|
|
||||||
|
override val filters: Array<String>
|
||||||
|
get() = classes
|
||||||
|
|
||||||
|
@Parameters(description = ["The partial or fully qualified names of the Java classes to inspect."])
|
||||||
|
var classes: Array<String> = emptyArray()
|
||||||
|
|
||||||
|
override fun processClasses(classes: List<Class<*>>) {
|
||||||
|
val sources = classes.map { ClassSource.fromClassName(it.name) }
|
||||||
|
val (_, messages) = executor.validate(*sources.toTypedArray())
|
||||||
|
|
||||||
|
if (messages.isNotEmpty()) {
|
||||||
|
for (message in messages.sorted()) {
|
||||||
|
printInfo(" - $message")
|
||||||
|
}
|
||||||
|
printInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (classSource in sources) {
|
||||||
|
val loadedClass = executor.load(classSource)
|
||||||
|
printInfo("Class: ${loadedClass.name}")
|
||||||
|
printVerbose(" - Byte code size: ${loadedClass.byteCode.bytes.size}")
|
||||||
|
printVerbose(" - Has been modified: ${loadedClass.byteCode.isModified}")
|
||||||
|
printInfo()
|
||||||
|
|
||||||
|
Files.createTempFile("sandbox-", ".class").apply {
|
||||||
|
Files.write(this, loadedClass.byteCode.bytes)
|
||||||
|
ProcessBuilder("javap", "-c", this.toString()).apply {
|
||||||
|
inheritIO()
|
||||||
|
environment().putAll(System.getenv())
|
||||||
|
start().apply {
|
||||||
|
waitFor()
|
||||||
|
exitValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Files.delete(this)
|
||||||
|
}
|
||||||
|
printInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.tools.Utilities.workingDirectory
|
||||||
|
import picocli.CommandLine.Command
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "tree",
|
||||||
|
description = ["Show the hierarchy of the classes that have been created with the 'new' command."]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class TreeCommand : CommandBase() {
|
||||||
|
|
||||||
|
override fun validateArguments() = true
|
||||||
|
|
||||||
|
override fun handleCommand(): Boolean {
|
||||||
|
val path = workingDirectory.resolve("tmp")
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
printError("No classes have been created so far. Run `djvm new` to get started.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ProcessBuilder("find", ".", "-type", "f").apply {
|
||||||
|
inheritIO()
|
||||||
|
environment().putAll(System.getenv())
|
||||||
|
directory(path.toFile())
|
||||||
|
start().apply {
|
||||||
|
waitFor()
|
||||||
|
exitValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import com.jcabi.manifests.Manifests
|
||||||
|
import picocli.CommandLine.IVersionProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version number to use for the tool.
|
||||||
|
*/
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class VersionProvider : IVersionProvider {
|
||||||
|
override fun getVersion(): Array<String> = arrayOf(
|
||||||
|
Manifests.read("Corda-Release-Version")
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import picocli.CommandLine.Command
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "whitelist",
|
||||||
|
description = ["Utilities and commands related to the whitelist for the deterministic sandbox."],
|
||||||
|
subcommands = [
|
||||||
|
WhitelistGenerateCommand::class,
|
||||||
|
WhitelistShowCommand::class
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class WhitelistCommand : CommandBase()
|
@ -0,0 +1,91 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||||
|
import net.corda.djvm.analysis.AnalysisContext
|
||||||
|
import net.corda.djvm.analysis.ClassAndMemberVisitor
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.source.ClassSource
|
||||||
|
import picocli.CommandLine.*
|
||||||
|
import java.io.PrintStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "generate",
|
||||||
|
description = ["Generate and export whitelist from the class and member declarations provided in one or more " +
|
||||||
|
"JARs."]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class WhitelistGenerateCommand : CommandBase() {
|
||||||
|
|
||||||
|
@Parameters(description = ["The paths of the JARs that the whitelist is to be generated from."])
|
||||||
|
var paths: Array<Path> = emptyArray()
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-o", "--output"],
|
||||||
|
description = ["The file to which the whitelist will be written. If not provided, STDOUT will be used."]
|
||||||
|
)
|
||||||
|
var output: Path? = null
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visitMember(clazz: ClassRepresentation, member: Member) {
|
||||||
|
entries.add("${clazz.name}.${member.memberName}:${member.signature}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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("""
|
||||||
|
|java/.*
|
||||||
|
|javax/.*
|
||||||
|
|jdk/.*
|
||||||
|
|sun/.*
|
||||||
|
|---
|
||||||
|
""".trimMargin().trim())
|
||||||
|
printEntries(it, entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printEntries(System.out, entries)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printEntries(stream: PrintStream, entries: List<String>) {
|
||||||
|
for (entry in entries.sorted().distinct()) {
|
||||||
|
stream.println(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package net.corda.djvm.tools.cli
|
||||||
|
|
||||||
|
import picocli.CommandLine.*
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "show",
|
||||||
|
description = ["Print the whitelist used for the deterministic sandbox."]
|
||||||
|
)
|
||||||
|
@Suppress("KDocMissingDocumentation")
|
||||||
|
class WhitelistShowCommand : CommandBase() {
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-w", "--whitelist"],
|
||||||
|
description = ["Override the default whitelist. Use provided whitelist instead."]
|
||||||
|
)
|
||||||
|
var whitelist: Path? = null
|
||||||
|
|
||||||
|
@Parameters(description = ["Words or phrases to use to filter down the result."])
|
||||||
|
var filters: Array<String> = emptyArray()
|
||||||
|
|
||||||
|
override fun validateArguments() = true
|
||||||
|
|
||||||
|
override fun handleCommand(): Boolean {
|
||||||
|
val whitelist = whitelistFromPath(whitelist)
|
||||||
|
val filters = filters.map(String::toLowerCase)
|
||||||
|
whitelist.items
|
||||||
|
.filter { item -> filters.all { it in item.toLowerCase() } }
|
||||||
|
.forEach { println(it) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1
djvm/shell/.gitignore
vendored
Normal file
1
djvm/shell/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
djvm_completion
|
20
djvm/shell/djvm
Executable file
20
djvm/shell/djvm
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
file="${BASH_SOURCE[0]}"
|
||||||
|
linked_file="$(test -L "$file" && readlink "$file" || echo "$file")"
|
||||||
|
base_dir="$(cd "$(dirname "$linked_file")/../" && pwd)"
|
||||||
|
version="$(cat $base_dir/../build.gradle | sed -n 's/^[ ]*ext\.corda_release_version[ =]*"\([^"]*\)".*$/\1/p')"
|
||||||
|
jar_file="$base_dir/cli/build/libs/corda-djvm-$version-all.jar"
|
||||||
|
|
||||||
|
CLASSPATH="${CLASSPATH:-}"
|
||||||
|
|
||||||
|
DEBUG=`echo "${DEBUG:-0}" | sed 's/^[Nn][Oo]*$/0/g'`
|
||||||
|
DEBUG_PORT=5005
|
||||||
|
DEBUG_AGENT=""
|
||||||
|
|
||||||
|
if [ "$DEBUG" != 0 ]; then
|
||||||
|
echo "Opening remote debugging session on port $DEBUG_PORT"
|
||||||
|
DEBUG_AGENT="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=$DEBUG_PORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
java $DEBUG_AGENT -cp "$CLASSPATH:.:tmp:$jar_file" net.corda.djvm.tools.cli.Program "$@"
|
17
djvm/shell/install
Executable file
17
djvm/shell/install
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
file="${BASH_SOURCE[0]}"
|
||||||
|
base_dir="$(cd "$(dirname "$file")/" && pwd)"
|
||||||
|
version="$(cat $base_dir/../../build.gradle | sed -n 's/^[ ]*ext\.corda_release_version[ =]*"\([^"]*\)".*$/\1/p')"
|
||||||
|
|
||||||
|
# Build DJVM module and CLI
|
||||||
|
cd "$base_dir/.."
|
||||||
|
../gradlew shadowJar
|
||||||
|
|
||||||
|
# Generate auto-completion file for Bash and ZSH
|
||||||
|
cd "$base_dir"
|
||||||
|
java -cp "$base_dir/../cli/build/libs/corda-djvm-$version-all.jar" \
|
||||||
|
picocli.AutoComplete -n djvm net.corda.djvm.tools.cli.Commands -f
|
||||||
|
|
||||||
|
# Create a symbolic link to the `djvm` utility
|
||||||
|
sudo ln -sf "$base_dir/djvm" /usr/local/bin/djvm
|
60
djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt
Normal file
60
djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package net.corda.djvm
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||||
|
import net.corda.djvm.code.DefinitionProvider
|
||||||
|
import net.corda.djvm.code.Emitter
|
||||||
|
import net.corda.djvm.execution.ExecutionProfile
|
||||||
|
import net.corda.djvm.rules.Rule
|
||||||
|
import net.corda.djvm.utilities.Discovery
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration to use for the deterministic sandbox.
|
||||||
|
*
|
||||||
|
* @property rules The rules to apply during the analysis phase.
|
||||||
|
* @property emitters The code emitters / re-writers to apply to all loaded classes.
|
||||||
|
* @property definitionProviders The meta-data providers to apply to class and member definitions.
|
||||||
|
* @property executionProfile The execution profile to use in the sandbox.
|
||||||
|
* @property analysisConfiguration The configuration used in the analysis of classes.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
class SandboxConfiguration private constructor(
|
||||||
|
val rules: List<Rule>,
|
||||||
|
val emitters: List<Emitter>,
|
||||||
|
val definitionProviders: List<DefinitionProvider>,
|
||||||
|
val executionProfile: ExecutionProfile,
|
||||||
|
val analysisConfiguration: AnalysisConfiguration
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Default configuration for the deterministic sandbox.
|
||||||
|
*/
|
||||||
|
val DEFAULT = SandboxConfiguration.of()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration with no emitters, rules, meta-data providers or runtime thresholds.
|
||||||
|
*/
|
||||||
|
val EMPTY = SandboxConfiguration.of(
|
||||||
|
ExecutionProfile.UNLIMITED, emptyList(), emptyList(), emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a sandbox configuration where one or more properties deviates from the default.
|
||||||
|
*/
|
||||||
|
fun of(
|
||||||
|
profile: ExecutionProfile = ExecutionProfile.DEFAULT,
|
||||||
|
rules: List<Rule> = Discovery.find(),
|
||||||
|
emitters: List<Emitter>? = null,
|
||||||
|
definitionProviders: List<DefinitionProvider> = Discovery.find(),
|
||||||
|
enableTracing: Boolean = true,
|
||||||
|
analysisConfiguration: AnalysisConfiguration = AnalysisConfiguration()
|
||||||
|
) = SandboxConfiguration(
|
||||||
|
executionProfile = profile,
|
||||||
|
rules = rules,
|
||||||
|
emitters = (emitters ?: Discovery.find()).filter {
|
||||||
|
enableTracing || !it.isTracer
|
||||||
|
},
|
||||||
|
definitionProviders = definitionProviders,
|
||||||
|
analysisConfiguration = analysisConfiguration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
62
djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt
Normal file
62
djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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>
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class loader to use inside the sandbox.
|
||||||
|
*/
|
||||||
|
val classLoader: SandboxClassLoader = SandboxClassLoader(
|
||||||
|
configuration,
|
||||||
|
AnalysisContext.fromConfiguration(configuration.analysisConfiguration, inputClasses)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A summary of the currently accumulated runtime costs (for, e.g., memory allocations, invocations, etc.).
|
||||||
|
*/
|
||||||
|
val runtimeCosts = RuntimeCostSummary(configuration.executionProfile)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a set of actions within the provided sandbox context.
|
||||||
|
*/
|
||||||
|
fun use(action: SandboxRuntimeContext.() -> Unit) {
|
||||||
|
SandboxRuntimeContext.instance = this
|
||||||
|
try {
|
||||||
|
this.action()
|
||||||
|
} finally {
|
||||||
|
threadLocalContext.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val threadLocalContext = object : ThreadLocal<SandboxRuntimeContext?>() {
|
||||||
|
override fun initialValue(): SandboxRuntimeContext? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When called from within a sandbox, this returns the context for the current sandbox thread.
|
||||||
|
*/
|
||||||
|
var instance: SandboxRuntimeContext
|
||||||
|
get() = threadLocalContext.get()
|
||||||
|
?: throw IllegalStateException("SandboxContext has not been initialized before use")
|
||||||
|
private set(value) {
|
||||||
|
threadLocalContext.set(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
import net.corda.djvm.messages.Severity
|
||||||
|
import net.corda.djvm.references.ClassModule
|
||||||
|
import net.corda.djvm.references.MemberModule
|
||||||
|
import sandbox.net.corda.djvm.costing.RuntimeCostAccounter
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration to use for an analysis.
|
||||||
|
*
|
||||||
|
* @property whitelist The whitelist of class names.
|
||||||
|
* @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.
|
||||||
|
* @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.
|
||||||
|
* @property classModule Module for handling evolution of a class hierarchy during analysis.
|
||||||
|
* @property memberModule Module for handling the specification and inspection of class members.
|
||||||
|
*/
|
||||||
|
class AnalysisConfiguration(
|
||||||
|
val whitelist: Whitelist = Whitelist.MINIMAL,
|
||||||
|
additionalPinnedClasses: Set<String> = emptySet(),
|
||||||
|
val minimumSeverityLevel: Severity = Severity.WARNING,
|
||||||
|
val classPath: List<Path> = emptyList(),
|
||||||
|
val analyzeAnnotations: Boolean = false,
|
||||||
|
val prefixFilters: List<String> = emptyList(),
|
||||||
|
val classModule: ClassModule = ClassModule(),
|
||||||
|
val memberModule: MemberModule = MemberModule()
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functionality used to resolve the qualified name and relevant information about a class.
|
||||||
|
*/
|
||||||
|
val classResolver: ClassResolver = ClassResolver(pinnedClasses, whitelist, SANDBOX_PREFIX)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @property messages Collection of messages gathered as part of the analysis.
|
||||||
|
* @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>
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val origins = mutableMapOf<String, MutableSet<EntityReference>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a class origin in the current analysis context.
|
||||||
|
*/
|
||||||
|
fun recordClassOrigin(name: String, origin: EntityReference) {
|
||||||
|
origins.getOrPut(name.normalize()) { mutableSetOf() }.add(origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of class origins. The resulting set represents the types referencing the class in question.
|
||||||
|
*/
|
||||||
|
val classOrigins: Map<String, Set<EntityReference>>
|
||||||
|
get() = origins
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new analysis context from provided configuration.
|
||||||
|
*/
|
||||||
|
fun fromConfiguration(configuration: AnalysisConfiguration, classes: List<ClassSource>): AnalysisContext {
|
||||||
|
return AnalysisContext(
|
||||||
|
MessageCollection(configuration.minimumSeverityLevel, configuration.prefixFilters),
|
||||||
|
ClassHierarchy(configuration.classModule, configuration.memberModule),
|
||||||
|
ReferenceMap(configuration.classModule),
|
||||||
|
classes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local extension method for normalizing a class name.
|
||||||
|
*/
|
||||||
|
private fun String.normalize() = this.replace("/", ".")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
import net.corda.djvm.messages.MessageCollection
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context of a class analysis.
|
||||||
|
*
|
||||||
|
* @property clazz The class currently being analyzed.
|
||||||
|
* @property member The member currently being analyzed.
|
||||||
|
* @property location The current source location.
|
||||||
|
* @property messages Collection of messages gathered as part of the analysis.
|
||||||
|
* @property configuration The configuration used in the analysis.
|
||||||
|
*/
|
||||||
|
data class AnalysisRuntimeContext(
|
||||||
|
val clazz: ClassRepresentation,
|
||||||
|
val member: Member?,
|
||||||
|
val location: SourceLocation,
|
||||||
|
val messages: MessageCollection,
|
||||||
|
val configuration: AnalysisConfiguration
|
||||||
|
)
|
@ -0,0 +1,548 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
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 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
|
||||||
|
*/
|
||||||
|
open class ClassAndMemberVisitor(
|
||||||
|
private val classVisitor: ClassVisitor? = null,
|
||||||
|
private val configuration: AnalysisConfiguration = AnalysisConfiguration()
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a reference to the currently used analysis context.
|
||||||
|
*/
|
||||||
|
protected var analysisContext: AnalysisContext =
|
||||||
|
AnalysisContext.fromConfiguration(configuration, emptyList())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a link to the class currently being traversed.
|
||||||
|
*/
|
||||||
|
private var currentClass: ClassRepresentation? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a link to the member currently being traversed.
|
||||||
|
*/
|
||||||
|
private var currentMember: Member? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current source location.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
inline fun <reified T> analyze(context: AnalysisContext) = analyze(T::class.java.name, context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze class by using the provided qualified name of the class.
|
||||||
|
*
|
||||||
|
* @param className The full, qualified name of the class.
|
||||||
|
* @param context The context in which the analysis is conducted.
|
||||||
|
* @param origin The originating class for the analysis.
|
||||||
|
*/
|
||||||
|
fun analyze(className: String, context: AnalysisContext, origin: String? = null) {
|
||||||
|
supportingClassLoader.classReader(className, context, origin).apply {
|
||||||
|
analyze(this, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze class by using the provided stream of its byte code.
|
||||||
|
*
|
||||||
|
* @param classStream A stream of the class' byte code.
|
||||||
|
* @param context The context in which the analysis is conducted.
|
||||||
|
*/
|
||||||
|
fun analyze(classStream: InputStream, context: AnalysisContext) =
|
||||||
|
analyze(ClassReader(classStream), context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze class by using the provided class reader.
|
||||||
|
*
|
||||||
|
* @param classReader An instance of the class reader to use to access the byte code.
|
||||||
|
* @param context The context in which to analyse the provided class.
|
||||||
|
* @param options Options for how to parse and process the class.
|
||||||
|
*/
|
||||||
|
fun analyze(classReader: ClassReader, context: AnalysisContext, options: Int = 0) {
|
||||||
|
analysisContext = context
|
||||||
|
classReader.accept(ClassVisitorImpl(classVisitor), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed class.
|
||||||
|
*/
|
||||||
|
open fun visitClass(clazz: ClassRepresentation): ClassRepresentation = clazz
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process class after it has been fully traversed and analyzed.
|
||||||
|
*/
|
||||||
|
open fun visitClassEnd(clazz: ClassRepresentation) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the meta-data indicating the source file of the traversed class (i.e., where it is compiled from).
|
||||||
|
*/
|
||||||
|
open fun visitSource(clazz: ClassRepresentation, source: String) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed class annotation.
|
||||||
|
*/
|
||||||
|
open fun visitClassAnnotation(clazz: ClassRepresentation, descriptor: String) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed member annotation.
|
||||||
|
*/
|
||||||
|
open fun visitMemberAnnotation(clazz: ClassRepresentation, member: Member, descriptor: String) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed method.
|
||||||
|
*/
|
||||||
|
open fun visitMethod(clazz: ClassRepresentation, method: Member): Member = method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed field.
|
||||||
|
*/
|
||||||
|
open fun visitField(clazz: ClassRepresentation, field: Member): Member = field
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed instruction.
|
||||||
|
*/
|
||||||
|
open fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the analysis context to pass on to method and field visitors.
|
||||||
|
*/
|
||||||
|
protected fun currentAnalysisContext(): AnalysisRuntimeContext {
|
||||||
|
return AnalysisRuntimeContext(
|
||||||
|
currentClass!!,
|
||||||
|
currentMember,
|
||||||
|
sourceLocation,
|
||||||
|
analysisContext.messages,
|
||||||
|
configuration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a class should be processed or not.
|
||||||
|
*/
|
||||||
|
protected fun shouldBeProcessed(className: String): Boolean {
|
||||||
|
return !configuration.whitelist.inNamespace(className) &&
|
||||||
|
className !in configuration.pinnedClasses
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed member annotation.
|
||||||
|
*/
|
||||||
|
private fun visitMemberAnnotation(
|
||||||
|
descriptor: String,
|
||||||
|
referencedClass: ClassRepresentation? = null,
|
||||||
|
referencedMember: Member? = null
|
||||||
|
) {
|
||||||
|
val clazz = (referencedClass ?: currentClass) ?: return
|
||||||
|
val member = (referencedMember ?: currentMember) ?: return
|
||||||
|
member.annotations.add(descriptor)
|
||||||
|
captureExceptions {
|
||||||
|
visitMemberAnnotation(clazz, member, descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run action with a guard that populates [messages] based on the output.
|
||||||
|
*/
|
||||||
|
private inline fun captureExceptions(action: () -> Unit): Boolean {
|
||||||
|
return try {
|
||||||
|
action()
|
||||||
|
true
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
recordMessage(exception, currentAnalysisContext())
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a message derived from a [Throwable].
|
||||||
|
*/
|
||||||
|
private fun recordMessage(exception: Throwable, context: AnalysisRuntimeContext) {
|
||||||
|
context.messages.add(Message.fromThrowable(exception, context.location))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a reference to a class.
|
||||||
|
*/
|
||||||
|
private fun recordTypeReference(type: String) {
|
||||||
|
val typeName = configuration.classModule
|
||||||
|
.normalizeClassName(type)
|
||||||
|
.replace("[]", "")
|
||||||
|
if (shouldBeProcessed(currentClass!!.name)) {
|
||||||
|
val classReference = ClassReference(typeName)
|
||||||
|
analysisContext.references.add(classReference, sourceLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a reference to a class member.
|
||||||
|
*/
|
||||||
|
private fun recordMemberReference(owner: String, name: String, desc: String) {
|
||||||
|
if (shouldBeProcessed(currentClass!!.name)) {
|
||||||
|
recordTypeReference(owner)
|
||||||
|
val memberReference = MemberReference(owner, name, desc)
|
||||||
|
analysisContext.references.add(memberReference, sourceLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visitor used to traverse and analyze a class.
|
||||||
|
*/
|
||||||
|
private inner class ClassVisitorImpl(
|
||||||
|
targetVisitor: ClassVisitor?
|
||||||
|
) : ClassVisitor(API_VERSION, targetVisitor) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed class.
|
||||||
|
*/
|
||||||
|
override fun visit(
|
||||||
|
version: Int, access: Int, name: String, signature: String?, superName: String?,
|
||||||
|
interfaces: Array<String>?
|
||||||
|
) {
|
||||||
|
val superClassName = superName ?: ""
|
||||||
|
val interfaceNames = interfaces?.toMutableList() ?: mutableListOf()
|
||||||
|
ClassRepresentation(version, access, name, superClassName, interfaceNames, genericsDetails = signature ?: "").also {
|
||||||
|
currentClass = it
|
||||||
|
currentMember = null
|
||||||
|
sourceLocation = SourceLocation(
|
||||||
|
className = name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
captureExceptions {
|
||||||
|
currentClass = visitClass(currentClass!!)
|
||||||
|
}
|
||||||
|
val visitedClass = currentClass!!
|
||||||
|
analysisContext.classes.add(visitedClass)
|
||||||
|
super.visit(
|
||||||
|
version, access, visitedClass.name, signature,
|
||||||
|
visitedClass.superClass.nullIfEmpty(),
|
||||||
|
visitedClass.interfaces.toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post-processing of the traversed class.
|
||||||
|
*/
|
||||||
|
override fun visitEnd() {
|
||||||
|
configuration.classModule
|
||||||
|
.getClassReferencesFromClass(currentClass!!, configuration.analyzeAnnotations)
|
||||||
|
.forEach { recordTypeReference(it) }
|
||||||
|
captureExceptions {
|
||||||
|
visitClassEnd(currentClass!!)
|
||||||
|
}
|
||||||
|
super.visitEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the meta-data indicating the source file of the traversed class (i.e., where it is compiled from).
|
||||||
|
*/
|
||||||
|
override fun visitSource(source: String?, debug: String?) {
|
||||||
|
currentClass!!.apply {
|
||||||
|
sourceFile = configuration.classModule.getFullSourceLocation(this, source)
|
||||||
|
sourceLocation = sourceLocation.copy(sourceFile = sourceFile)
|
||||||
|
captureExceptions {
|
||||||
|
visitSource(this, sourceFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.visitSource(source, debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided annotations.
|
||||||
|
*/
|
||||||
|
override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor? {
|
||||||
|
currentClass!!.apply {
|
||||||
|
annotations.add(desc)
|
||||||
|
captureExceptions {
|
||||||
|
visitClassAnnotation(this, desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visitAnnotation(desc, visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed method.
|
||||||
|
*/
|
||||||
|
override fun visitMethod(
|
||||||
|
access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?
|
||||||
|
): MethodVisitor? {
|
||||||
|
var visitedMember: Member? = null
|
||||||
|
val clazz = currentClass!!
|
||||||
|
val member = Member(access, clazz.name, name, desc, signature ?: "")
|
||||||
|
currentMember = member
|
||||||
|
sourceLocation = sourceLocation.copy(
|
||||||
|
memberName = name,
|
||||||
|
signature = desc,
|
||||||
|
lineNumber = 0
|
||||||
|
)
|
||||||
|
val processMember = captureExceptions {
|
||||||
|
visitedMember = visitMethod(clazz, member)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about the traversed field.
|
||||||
|
*/
|
||||||
|
override fun visitField(
|
||||||
|
access: Int, name: String, desc: String, signature: String?, value: Any?
|
||||||
|
): FieldVisitor? {
|
||||||
|
var visitedMember: Member? = null
|
||||||
|
val clazz = currentClass!!
|
||||||
|
val member = Member(access, clazz.name, name, desc, "", value = value)
|
||||||
|
currentMember = member
|
||||||
|
sourceLocation = sourceLocation.copy(
|
||||||
|
memberName = name,
|
||||||
|
signature = desc,
|
||||||
|
lineNumber = 0
|
||||||
|
)
|
||||||
|
val processMember = captureExceptions {
|
||||||
|
visitedMember = visitField(clazz, member)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visitor used to traverse and analyze a method.
|
||||||
|
*/
|
||||||
|
private inner class MethodVisitorImpl(
|
||||||
|
targetVisitor: MethodVisitor?
|
||||||
|
) : MethodVisitor(API_VERSION, targetVisitor) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record line number of current instruction.
|
||||||
|
*/
|
||||||
|
override fun visitLineNumber(line: Int, start: Label?) {
|
||||||
|
sourceLocation = sourceLocation.copy(lineNumber = line)
|
||||||
|
super.visitLineNumber(line, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided label.
|
||||||
|
*/
|
||||||
|
override fun visitLabel(label: Label) {
|
||||||
|
visit(CodeLabel(label), defaultFirst = true) {
|
||||||
|
super.visitLabel(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided annotations.
|
||||||
|
*/
|
||||||
|
override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor? {
|
||||||
|
visitMemberAnnotation(desc)
|
||||||
|
return super.visitAnnotation(desc, visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided field access instruction.
|
||||||
|
*/
|
||||||
|
override fun visitFieldInsn(opcode: Int, owner: String, name: String, desc: String) {
|
||||||
|
recordMemberReference(owner, name, desc)
|
||||||
|
visit(MemberAccessInstruction(opcode, owner, name, desc, isMethod = false)) {
|
||||||
|
super.visitFieldInsn(opcode, owner, name, desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided method invocation instruction.
|
||||||
|
*/
|
||||||
|
override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) {
|
||||||
|
recordMemberReference(owner, name, desc)
|
||||||
|
visit(MemberAccessInstruction(opcode, owner, name, desc, itf, isMethod = true)) {
|
||||||
|
super.visitMethodInsn(opcode, owner, name, desc, itf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided dynamic invocation instruction.
|
||||||
|
*/
|
||||||
|
override fun visitInvokeDynamicInsn(name: String, desc: String, bsm: Handle?, vararg bsmArgs: Any?) {
|
||||||
|
val module = configuration.memberModule
|
||||||
|
visit(DynamicInvocationInstruction(
|
||||||
|
name, desc, module.numberOfArguments(desc), module.returnsValueOrReference(desc)
|
||||||
|
)) {
|
||||||
|
super.visitInvokeDynamicInsn(name, desc, bsm, *bsmArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided jump instruction.
|
||||||
|
*/
|
||||||
|
override fun visitJumpInsn(opcode: Int, label: Label) {
|
||||||
|
visit(BranchInstruction(opcode, label)) {
|
||||||
|
super.visitJumpInsn(opcode, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided instruction (general instruction with no operands).
|
||||||
|
*/
|
||||||
|
override fun visitInsn(opcode: Int) {
|
||||||
|
visit(Instruction(opcode)) {
|
||||||
|
super.visitInsn(opcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided instruction (general instruction with one operand).
|
||||||
|
*/
|
||||||
|
override fun visitIntInsn(opcode: Int, operand: Int) {
|
||||||
|
visit(IntegerInstruction(opcode, operand)) {
|
||||||
|
super.visitIntInsn(opcode, operand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided type instruction (e.g., [Opcodes.NEW], [Opcodes.ANEWARRAY],
|
||||||
|
* [Opcodes.INSTANCEOF] and [Opcodes.CHECKCAST]).
|
||||||
|
*/
|
||||||
|
override fun visitTypeInsn(opcode: Int, type: String) {
|
||||||
|
recordTypeReference(type)
|
||||||
|
visit(TypeInstruction(opcode, type)) {
|
||||||
|
try {
|
||||||
|
super.visitTypeInsn(opcode, type)
|
||||||
|
} catch (exception: IllegalArgumentException) {
|
||||||
|
throw IllegalArgumentException("Invalid name used in type instruction; $type", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided try-catch/finally block.
|
||||||
|
*/
|
||||||
|
override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, type: String?) {
|
||||||
|
val block = if (type != null) {
|
||||||
|
TryCatchBlock(type, handler)
|
||||||
|
} else {
|
||||||
|
TryFinallyBlock(handler)
|
||||||
|
}
|
||||||
|
visit(block) {
|
||||||
|
super.visitTryCatchBlock(start, end, handler, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided table switch instruction.
|
||||||
|
*/
|
||||||
|
override fun visitTableSwitchInsn(min: Int, max: Int, dflt: Label, vararg labels: Label) {
|
||||||
|
visit(TableSwitchInstruction(min, max, dflt, labels.toList())) {
|
||||||
|
super.visitTableSwitchInsn(min, max, dflt, *labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided increment instruction.
|
||||||
|
*/
|
||||||
|
override fun visitIincInsn(`var`: Int, increment: Int) {
|
||||||
|
visit(IntegerInstruction(Opcodes.IINC, increment)) {
|
||||||
|
super.visitIincInsn(`var`, increment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function used to streamline the access to an instruction and to catch any related processing errors.
|
||||||
|
*/
|
||||||
|
private inline fun visit(instruction: Instruction, defaultFirst: Boolean = false, defaultAction: () -> Unit) {
|
||||||
|
val emitterModule = EmitterModule(mv ?: StubMethodVisitor())
|
||||||
|
if (defaultFirst) {
|
||||||
|
defaultAction()
|
||||||
|
}
|
||||||
|
val success = captureExceptions {
|
||||||
|
visitInstruction(currentMember!!, emitterModule, instruction)
|
||||||
|
}
|
||||||
|
if (!defaultFirst) {
|
||||||
|
if (success && emitterModule.emitDefaultInstruction) {
|
||||||
|
defaultAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visitor used to traverse and analyze a field.
|
||||||
|
*/
|
||||||
|
private inner class FieldVisitorImpl(
|
||||||
|
targetVisitor: FieldVisitor?
|
||||||
|
) : FieldVisitor(API_VERSION, targetVisitor) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract information about provided annotations.
|
||||||
|
*/
|
||||||
|
override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor? {
|
||||||
|
visitMemberAnnotation(desc)
|
||||||
|
return super.visitAnnotation(desc, visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StubMethodVisitor : MethodVisitor(API_VERSION, null)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The API version of ASM.
|
||||||
|
*/
|
||||||
|
const val API_VERSION: Int = Opcodes.ASM6
|
||||||
|
|
||||||
|
private fun String.nullIfEmpty(): String? {
|
||||||
|
return if (this.isEmpty()) { null } else { this }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
128
djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt
Normal file
128
djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functionality for resolving the class name of a sandboxable class.
|
||||||
|
*
|
||||||
|
* The resolution of a class name entails determining whether the class can be instrumented or not. This means that the
|
||||||
|
* following criteria need to be satisfied:
|
||||||
|
* - The class do not reside in the "java/lang" package.
|
||||||
|
* - The class has not been explicitly pinned.
|
||||||
|
* - The class does not already reside in the top-level package named [sandboxPrefix].
|
||||||
|
*
|
||||||
|
* If these criteria have been satisfied, the fully-qualified class name will be derived by prepending [sandboxPrefix]
|
||||||
|
* to it. Note that [ClassLoader] will not allow defining a class in a package whose fully-qualified class name starts
|
||||||
|
* with "java/". That will result in the class loader throwing [SecurityException]. Also, some values map onto types
|
||||||
|
* defined in "java/lang/", e.g., [Integer] and [String]. These cannot be trivially moved into a different package due
|
||||||
|
* to the internal mechanisms of the JVM.
|
||||||
|
*
|
||||||
|
* @property pinnedClasses Classes that have already been declared in the sandbox namespace and that should be made
|
||||||
|
* available inside the sandboxed environment.
|
||||||
|
* @property whitelist The set of classes in the Java runtime libraries that have been whitelisted and that should be
|
||||||
|
* left alone.
|
||||||
|
* @property sandboxPrefix The package name prefix to use for classes loaded into a sandbox.
|
||||||
|
*/
|
||||||
|
class ClassResolver(
|
||||||
|
private val pinnedClasses: Set<String>,
|
||||||
|
private val whitelist: Whitelist,
|
||||||
|
private val sandboxPrefix: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the class name from a fully qualified name.
|
||||||
|
*/
|
||||||
|
fun resolve(name: String): String {
|
||||||
|
return when {
|
||||||
|
name.startsWith("[") && name.endsWith(";") -> {
|
||||||
|
complexArrayTypeRegex.replace(name) {
|
||||||
|
"${it.groupValues[1]}L${resolveName(it.groupValues[2])};"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name.startsWith("[") && !name.endsWith(";") -> name
|
||||||
|
else -> resolveName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the class name from a fully qualified normalized name.
|
||||||
|
*/
|
||||||
|
fun resolveNormalized(name: String): String {
|
||||||
|
return resolve(name.replace('.', '/')).replace('/', '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive descriptor by resolving all referenced class names.
|
||||||
|
*/
|
||||||
|
fun resolveDescriptor(descriptor: String): String {
|
||||||
|
val outputDescriptor = StringBuilder()
|
||||||
|
var longName = StringBuilder()
|
||||||
|
var isProcessingLongName = false
|
||||||
|
loop@ for (char in descriptor) {
|
||||||
|
when {
|
||||||
|
char != ';' && isProcessingLongName -> {
|
||||||
|
longName.append(char)
|
||||||
|
continue@loop
|
||||||
|
}
|
||||||
|
char == 'L' -> {
|
||||||
|
isProcessingLongName = true
|
||||||
|
longName = StringBuilder()
|
||||||
|
}
|
||||||
|
char == ';' -> {
|
||||||
|
outputDescriptor.append(resolve(longName.toString()))
|
||||||
|
isProcessingLongName = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputDescriptor.append(char)
|
||||||
|
}
|
||||||
|
return outputDescriptor.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the resolution of a class name.
|
||||||
|
*/
|
||||||
|
fun reverse(resolvedClassName: String): String {
|
||||||
|
if (resolvedClassName in pinnedClasses) {
|
||||||
|
return resolvedClassName
|
||||||
|
}
|
||||||
|
if (resolvedClassName.startsWith(sandboxPrefix)) {
|
||||||
|
val nameWithoutPrefix = resolvedClassName.drop(sandboxPrefix.length)
|
||||||
|
if (resolve(nameWithoutPrefix) == resolvedClassName) {
|
||||||
|
return nameWithoutPrefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvedClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the resolution of a class name from a fully qualified normalized name.
|
||||||
|
*/
|
||||||
|
fun reverseNormalized(name: String): String {
|
||||||
|
return reverse(name.replace('.', '/')).replace('/', '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve class name from a fully qualified name.
|
||||||
|
*/
|
||||||
|
private fun resolveName(name: String): String {
|
||||||
|
return if (isPinnedOrWhitelistedClass(name)) {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
"$sandboxPrefix$name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if class is whitelisted or pinned.
|
||||||
|
*/
|
||||||
|
private fun isPinnedOrWhitelistedClass(name: String): Boolean {
|
||||||
|
return whitelist.matches(name) ||
|
||||||
|
name in pinnedClasses ||
|
||||||
|
sandboxRegex.matches(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sandboxRegex = "^$sandboxPrefix.*$".toRegex()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val complexArrayTypeRegex = "^(\\[+)L(.*);$".toRegex()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt
Normal file
38
djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trie data structure to make prefix matching more efficient.
|
||||||
|
*/
|
||||||
|
class PrefixTree {
|
||||||
|
|
||||||
|
private class Node(val children: MutableMap<Char, Node> = mutableMapOf())
|
||||||
|
|
||||||
|
private val root = Node()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new prefix to the set.
|
||||||
|
*/
|
||||||
|
fun add(prefix: String) {
|
||||||
|
var node = root
|
||||||
|
for (char in prefix) {
|
||||||
|
val nextNode = node.children.computeIfAbsent(char) { Node() }
|
||||||
|
node = nextNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any of the registered prefixes matches the provided string.
|
||||||
|
*/
|
||||||
|
fun contains(string: String): Boolean {
|
||||||
|
var node = root
|
||||||
|
for (char in string) {
|
||||||
|
val nextNode = node.children[char] ?: return false
|
||||||
|
if (nextNode.children.isEmpty()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
node = nextNode
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
import net.corda.djvm.formatting.MemberFormatter
|
||||||
|
import net.corda.djvm.references.MemberInformation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of the source location of a class, member or instruction.
|
||||||
|
*
|
||||||
|
* @property className The name of the class.
|
||||||
|
* @property sourceFile The file containing the source of the compiled class.
|
||||||
|
* @property memberName The name of the field or method.
|
||||||
|
* @property signature The signature of the field or method.
|
||||||
|
* @property lineNumber The index of the line from which the instruction was compiled.
|
||||||
|
*/
|
||||||
|
data class SourceLocation(
|
||||||
|
override val className: String = "",
|
||||||
|
val sourceFile: String = "",
|
||||||
|
override val memberName: String = "",
|
||||||
|
override val signature: String = "",
|
||||||
|
val lineNumber: Int = 0
|
||||||
|
) : MemberInformation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not information about the source file is available.
|
||||||
|
*/
|
||||||
|
val hasSourceFile: Boolean
|
||||||
|
get() = sourceFile.isNotBlank()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not information about the line number is available.
|
||||||
|
*/
|
||||||
|
val hasLineNumber: Boolean
|
||||||
|
get() = lineNumber != 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string representation of the source location.
|
||||||
|
*/
|
||||||
|
override fun toString(): String {
|
||||||
|
return StringBuilder().apply {
|
||||||
|
append(className.removePrefix("sandbox/"))
|
||||||
|
if (memberName.isNotBlank()) {
|
||||||
|
append(".$memberName")
|
||||||
|
if (memberFormatter.isMethod(signature)) {
|
||||||
|
append("(${memberFormatter.format(signature)})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a formatted string representation of the source location.
|
||||||
|
*/
|
||||||
|
fun format(): String {
|
||||||
|
if (className.isBlank()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return StringBuilder().apply {
|
||||||
|
append("@|blue ")
|
||||||
|
append(if (hasSourceFile) {
|
||||||
|
sourceFile
|
||||||
|
} else {
|
||||||
|
className
|
||||||
|
}.removePrefix("sandbox/"))
|
||||||
|
append("|@")
|
||||||
|
if (hasLineNumber) {
|
||||||
|
append(" on @|yellow line $lineNumber|@")
|
||||||
|
}
|
||||||
|
if (memberName.isNotBlank()) {
|
||||||
|
append(" in @|green ")
|
||||||
|
if (hasSourceFile) {
|
||||||
|
append("${memberFormatter.getShortClassName(className)}.$memberName")
|
||||||
|
} else {
|
||||||
|
append(memberName)
|
||||||
|
}
|
||||||
|
if (memberFormatter.isMethod(signature)) {
|
||||||
|
append("(${memberFormatter.format(signature)})")
|
||||||
|
}
|
||||||
|
append("|@")
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
private val memberFormatter = MemberFormatter()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
205
djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt
Normal file
205
djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package net.corda.djvm.analysis
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.PushbackInputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a whitelist deciding what classes, interfaces and members are permissible and consequently can be
|
||||||
|
* referenced from sandboxed code.
|
||||||
|
*
|
||||||
|
* @property namespace If provided, this parameter bounds the namespace of the whitelist.
|
||||||
|
* @property entries A set of regular expressions used to determine whether a name is covered by the whitelist or not.
|
||||||
|
* @property textEntries A set of textual entries used to determine whether a name is covered by the whitelist or not.
|
||||||
|
*/
|
||||||
|
open class Whitelist private constructor(
|
||||||
|
private val namespace: Whitelist? = null,
|
||||||
|
private val entries: Set<Regex>,
|
||||||
|
private val textEntries: Set<String>
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of seen names that matched with the whitelist.
|
||||||
|
*/
|
||||||
|
private val seenNames = mutableSetOf<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if name falls within the namespace of the whitelist.
|
||||||
|
*/
|
||||||
|
fun inNamespace(name: String): Boolean {
|
||||||
|
return namespace != null && namespace.matches(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a name is covered by the whitelist.
|
||||||
|
*/
|
||||||
|
fun matches(name: String): Boolean {
|
||||||
|
if (name in seenNames) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
name in textEntries -> {
|
||||||
|
seenNames.add(name)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
entries.any { it.matches(name) } -> {
|
||||||
|
seenNames.add(name)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine two whitelists into one.
|
||||||
|
*/
|
||||||
|
operator fun plus(whitelist: Whitelist): Whitelist {
|
||||||
|
val entries = entries + whitelist.entries
|
||||||
|
val textEntries = textEntries + whitelist.textEntries
|
||||||
|
return when {
|
||||||
|
namespace != null && whitelist.namespace != null ->
|
||||||
|
Whitelist(namespace + whitelist.namespace, entries, textEntries)
|
||||||
|
namespace != null ->
|
||||||
|
Whitelist(namespace, entries, textEntries)
|
||||||
|
whitelist.namespace != null ->
|
||||||
|
Whitelist(whitelist.namespace, entries, textEntries)
|
||||||
|
else ->
|
||||||
|
Whitelist(null, entries, textEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a derived whitelist by adding a set of additional entries.
|
||||||
|
*/
|
||||||
|
operator fun plus(additionalEntries: Set<Regex>): Whitelist {
|
||||||
|
return plus(Whitelist(null, additionalEntries, emptySet()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a derived whitelist by adding an additional entry.
|
||||||
|
*/
|
||||||
|
operator fun plus(additionalEntry: Regex): Whitelist {
|
||||||
|
return plus(setOf(additionalEntry))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerate all the entries of the whitelist.
|
||||||
|
*/
|
||||||
|
val items: Set<String>
|
||||||
|
get() = textEntries + entries.map { it.pattern }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val everythingRegex = setOf(".*".toRegex())
|
||||||
|
|
||||||
|
private val minimumSet = setOf(
|
||||||
|
"^java/lang/Boolean(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Byte(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Character(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Class(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/ClassLoader(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Cloneable(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Comparable(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Double(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Enum(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Float(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Integer(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Iterable(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Long(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Number(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Object(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Override(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Short(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/String(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/ThreadDeath(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Throwable(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/Void(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/.*Error(\\..*)?$".toRegex(),
|
||||||
|
"^java/lang/.*Exception(\\..*)?$".toRegex()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty whitelist.
|
||||||
|
*/
|
||||||
|
val EMPTY: Whitelist = Whitelist(null, emptySet(), emptySet())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum set of classes that needs to be pinned from standard Java libraries.
|
||||||
|
*/
|
||||||
|
val MINIMAL: Whitelist = Whitelist(Whitelist(null, minimumSet, emptySet()), minimumSet, emptySet())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelist everything.
|
||||||
|
*/
|
||||||
|
val EVERYTHING: Whitelist = Whitelist(
|
||||||
|
Whitelist(null, everythingRegex, emptySet()),
|
||||||
|
everythingRegex,
|
||||||
|
emptySet()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a whitelist from a resource stream.
|
||||||
|
*/
|
||||||
|
fun fromResource(resourceName: String): Whitelist {
|
||||||
|
val inputStream = Whitelist::class.java.getResourceAsStream("/$resourceName")
|
||||||
|
?: throw FileNotFoundException("Cannot find resource \"$resourceName\"")
|
||||||
|
return fromStream(inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a whitelist from a file.
|
||||||
|
*/
|
||||||
|
fun fromFile(file: Path): Whitelist {
|
||||||
|
return Files.newInputStream(file).use(this::fromStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a whitelist from a GZIP'ed or raw input stream.
|
||||||
|
*/
|
||||||
|
fun fromStream(inputStream: InputStream): Whitelist {
|
||||||
|
val namespaceEntries = mutableSetOf<Regex>()
|
||||||
|
val entries = mutableSetOf<String>()
|
||||||
|
decompressStream(inputStream).bufferedReader().use {
|
||||||
|
var isCollectingFilterEntries = false
|
||||||
|
for (line in it.lines().filter(String::isNotBlank)) {
|
||||||
|
when {
|
||||||
|
line.trim() == SECTION_SEPARATOR -> {
|
||||||
|
isCollectingFilterEntries = true
|
||||||
|
}
|
||||||
|
isCollectingFilterEntries -> entries.add(line)
|
||||||
|
else -> namespaceEntries.add(Regex(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val namespace = if (namespaceEntries.isNotEmpty()) {
|
||||||
|
Whitelist(null, namespaceEntries, emptySet())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return Whitelist(namespace = namespace, entries = emptySet(), textEntries = entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompress stream if GZIP'ed, otherwise, use the raw stream.
|
||||||
|
*/
|
||||||
|
private fun decompressStream(inputStream: InputStream): InputStream {
|
||||||
|
val rawStream = PushbackInputStream(inputStream, 2)
|
||||||
|
val signature = ByteArray(2)
|
||||||
|
val length = rawStream.read(signature)
|
||||||
|
rawStream.unread(signature, 0, length)
|
||||||
|
return if (signature[0] == GZIP_MAGIC_FIRST_BYTE && signature[1] == GZIP_MAGIC_SECOND_BYTE) {
|
||||||
|
GZIPInputStream(rawStream)
|
||||||
|
} else {
|
||||||
|
rawStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SECTION_SEPARATOR = "---"
|
||||||
|
private const val GZIP_MAGIC_FIRST_BYTE = GZIPInputStream.GZIP_MAGIC.toByte()
|
||||||
|
private const val GZIP_MAGIC_SECOND_BYTE = (GZIPInputStream.GZIP_MAGIC shr 8).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
|||||||
|
package net.corda.djvm.annotations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation for marking a class, field or method as non-deterministic.
|
||||||
|
*/
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.FILE,
|
||||||
|
AnnotationTarget.CLASS,
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.FIELD,
|
||||||
|
AnnotationTarget.CONSTRUCTOR,
|
||||||
|
AnnotationTarget.PROPERTY,
|
||||||
|
AnnotationTarget.PROPERTY_GETTER,
|
||||||
|
AnnotationTarget.PROPERTY_SETTER
|
||||||
|
)
|
||||||
|
annotation class NonDeterministic
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class definition provider is a hook for [ClassMutator], from where one can modify the name and meta-data of
|
||||||
|
* processed classes.
|
||||||
|
*/
|
||||||
|
interface ClassDefinitionProvider : DefinitionProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for providing modifications to a class definition.
|
||||||
|
*
|
||||||
|
* @param context The context in which the hook is called.
|
||||||
|
* @param clazz The original class definition.
|
||||||
|
*
|
||||||
|
* @return The updated class definition, or [clazz] if no changes are desired.
|
||||||
|
*/
|
||||||
|
fun define(context: AnalysisRuntimeContext, clazz: ClassRepresentation): ClassRepresentation
|
||||||
|
|
||||||
|
}
|
98
djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt
Normal file
98
djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||||
|
import net.corda.djvm.analysis.ClassAndMemberVisitor
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.utilities.Processor
|
||||||
|
import net.corda.djvm.utilities.loggerFor
|
||||||
|
import org.objectweb.asm.ClassVisitor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for applying a set of definition providers and emitters to a class or set of classes.
|
||||||
|
*
|
||||||
|
* @param classVisitor Class visitor to use when traversing the structure of classes.
|
||||||
|
* @property definitionProviders A set of providers used to update the name or meta-data of classes and members.
|
||||||
|
* @property emitters A set of code emitters used to modify and instrument method bodies.
|
||||||
|
*/
|
||||||
|
class ClassMutator(
|
||||||
|
classVisitor: ClassVisitor,
|
||||||
|
private val configuration: AnalysisConfiguration,
|
||||||
|
private val definitionProviders: List<DefinitionProvider> = emptyList(),
|
||||||
|
private val emitters: List<Emitter> = emptyList()
|
||||||
|
) : ClassAndMemberVisitor(classVisitor, configuration = configuration) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks whether any modifications have been applied to any of the processed class(es) and pertinent members.
|
||||||
|
*/
|
||||||
|
var hasBeenModified: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply definition providers to a class. This can be used to update the name or definition (pertinent meta-data)
|
||||||
|
* of the class itself.
|
||||||
|
*/
|
||||||
|
override fun visitClass(clazz: ClassRepresentation): ClassRepresentation {
|
||||||
|
var resultingClass = clazz
|
||||||
|
Processor.processEntriesOfType<ClassDefinitionProvider>(definitionProviders, analysisContext.messages) {
|
||||||
|
resultingClass = it.define(currentAnalysisContext(), resultingClass)
|
||||||
|
}
|
||||||
|
if (clazz != resultingClass) {
|
||||||
|
logger.trace("Type has been mutated {}", clazz)
|
||||||
|
hasBeenModified = true
|
||||||
|
}
|
||||||
|
return super.visitClass(resultingClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply definition providers to a method. This can be used to update the name or definition (pertinent meta-data)
|
||||||
|
* of a class member.
|
||||||
|
*/
|
||||||
|
override fun visitMethod(clazz: ClassRepresentation, method: Member): Member {
|
||||||
|
var resultingMethod = method
|
||||||
|
Processor.processEntriesOfType<MemberDefinitionProvider>(definitionProviders, analysisContext.messages) {
|
||||||
|
resultingMethod = it.define(currentAnalysisContext(), resultingMethod)
|
||||||
|
}
|
||||||
|
if (method != resultingMethod) {
|
||||||
|
logger.trace("Method has been mutated {}", method)
|
||||||
|
hasBeenModified = true
|
||||||
|
}
|
||||||
|
return super.visitMethod(clazz, resultingMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply definition providers to a field. This can be used to update the name or definition (pertinent meta-data)
|
||||||
|
* of a class member.
|
||||||
|
*/
|
||||||
|
override fun visitField(clazz: ClassRepresentation, field: Member): Member {
|
||||||
|
var resultingField = field
|
||||||
|
Processor.processEntriesOfType<MemberDefinitionProvider>(definitionProviders, analysisContext.messages) {
|
||||||
|
resultingField = it.define(currentAnalysisContext(), resultingField)
|
||||||
|
}
|
||||||
|
if (field != resultingField) {
|
||||||
|
logger.trace("Field has been mutated {}", field)
|
||||||
|
hasBeenModified = true
|
||||||
|
}
|
||||||
|
return super.visitField(clazz, resultingField)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply emitters to an instruction. This can be used to instrument a part of the code block, change behaviour of
|
||||||
|
* an existing instruction, or strip it out completely.
|
||||||
|
*/
|
||||||
|
override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) {
|
||||||
|
val context = EmitterContext(currentAnalysisContext(), configuration, emitter)
|
||||||
|
Processor.processEntriesOfType<Emitter>(emitters, analysisContext.messages) {
|
||||||
|
it.emit(context, instruction)
|
||||||
|
}
|
||||||
|
if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) {
|
||||||
|
hasBeenModified = true
|
||||||
|
}
|
||||||
|
super.visitInstruction(method, emitter, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val logger = loggerFor<ClassMutator>()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A definition provider is a hook for [ClassMutator], from where one can modify the name and meta-data of processed
|
||||||
|
* classes and class members.
|
||||||
|
*/
|
||||||
|
interface DefinitionProvider
|
26
djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt
Normal file
26
djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An emitter is a hook for [ClassMutator], from where one can modify the byte code of a class method.
|
||||||
|
*/
|
||||||
|
interface Emitter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for providing modifications to an instruction in a method body. One can also prepend and append instructions
|
||||||
|
* by using the [EmitterContext], and skip the default instruction altogether by invoking
|
||||||
|
* [EmitterModule.preventDefault] from within [EmitterContext.emit].
|
||||||
|
*
|
||||||
|
* @param context The context from which the emitter is invoked. By calling [EmitterContext.emit], one gets access
|
||||||
|
* to an instance of [EmitterModule] from within the supplied closure. From there, one can emit new instructions and
|
||||||
|
* intercept the original instruction (for instance, modify or delete the instruction).
|
||||||
|
* @param instruction The instruction currently being processed.
|
||||||
|
*/
|
||||||
|
fun emit(context: EmitterContext, instruction: Instruction)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox.
|
||||||
|
*/
|
||||||
|
val isTracer: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
}
|
76
djvm/src/main/kotlin/net/corda/djvm/code/EmitterContext.kt
Normal file
76
djvm/src/main/kotlin/net/corda/djvm/code/EmitterContext.kt
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisConfiguration
|
||||||
|
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||||
|
import net.corda.djvm.analysis.SourceLocation
|
||||||
|
import net.corda.djvm.analysis.Whitelist
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.ClassModule
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.references.MemberModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context in which an emitter is invoked.
|
||||||
|
*
|
||||||
|
* @property analysisContext The context in which a class and its members are processed.
|
||||||
|
* @property configuration The configuration to used for the analysis.
|
||||||
|
* @property emitterModule A module providing code generation functionality that can be used from within an emitter.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
open class EmitterContext(
|
||||||
|
private val analysisContext: AnalysisRuntimeContext,
|
||||||
|
private val configuration: AnalysisConfiguration,
|
||||||
|
val emitterModule: EmitterModule
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class currently being analysed.
|
||||||
|
*/
|
||||||
|
val clazz: ClassRepresentation
|
||||||
|
get() = analysisContext.clazz
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The member currently being analysed, if any.
|
||||||
|
*/
|
||||||
|
val member: Member?
|
||||||
|
get() = analysisContext.member
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current source location.
|
||||||
|
*/
|
||||||
|
val location: SourceLocation
|
||||||
|
get() = analysisContext.location
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configured whitelist.
|
||||||
|
*/
|
||||||
|
val whitelist: Whitelist
|
||||||
|
get() = analysisContext.configuration.whitelist
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for dealing with classes.
|
||||||
|
*/
|
||||||
|
val classModule: ClassModule
|
||||||
|
get() = analysisContext.configuration.classModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for dealing with members.
|
||||||
|
*/
|
||||||
|
val memberModule: MemberModule
|
||||||
|
get() = analysisContext.configuration.memberModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the sandboxed name of a class or interface.
|
||||||
|
*/
|
||||||
|
fun resolve(typeName: String): String {
|
||||||
|
return configuration.classResolver.resolve(typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up and execute an emitter block for a particular member.
|
||||||
|
*/
|
||||||
|
inline fun emit(action: EmitterModule.() -> Unit) {
|
||||||
|
action(emitterModule)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
125
djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt
Normal file
125
djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
import org.objectweb.asm.MethodVisitor
|
||||||
|
import org.objectweb.asm.Opcodes
|
||||||
|
import sandbox.net.corda.djvm.costing.RuntimeCostAccounter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper functions for emitting code to a method body.
|
||||||
|
*
|
||||||
|
* @property methodVisitor The underlying visitor which controls all the byte code for the current method.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
class EmitterModule(
|
||||||
|
private val methodVisitor: MethodVisitor
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the default instruction in the currently processed block is to be emitted or not.
|
||||||
|
*/
|
||||||
|
var emitDefaultInstruction: Boolean = true
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether any custom code has been emitted in the applicable context.
|
||||||
|
*/
|
||||||
|
var hasEmittedCustomCode: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for creating a new object of type [typeName].
|
||||||
|
*/
|
||||||
|
fun new(typeName: String, opcode: Int = Opcodes.NEW) {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
methodVisitor.visitTypeInsn(opcode, typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for creating a new object of type [T].
|
||||||
|
*/
|
||||||
|
inline fun <reified T> new() {
|
||||||
|
new(T::class.java.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for loading an integer constant onto the stack.
|
||||||
|
*/
|
||||||
|
fun loadConstant(constant: Int) {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
methodVisitor.visitLdcInsn(constant)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for loading a string constant onto the stack.
|
||||||
|
*/
|
||||||
|
fun loadConstant(constant: String) {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
methodVisitor.visitLdcInsn(constant)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for invoking a static method.
|
||||||
|
*/
|
||||||
|
fun invokeStatic(owner: String, name: String, descriptor: String, isInterface: Boolean = false) {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, isInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for invoking a special method, e.g. a constructor or a method on a super-type.
|
||||||
|
*/
|
||||||
|
fun invokeSpecial(owner: String, name: String, descriptor: String, isInterface: Boolean = false) {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
methodVisitor.visitMethodInsn(Opcodes.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for popping one element off the stack.
|
||||||
|
*/
|
||||||
|
fun pop() {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
methodVisitor.visitInsn(Opcodes.POP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for duplicating the top of the stack.
|
||||||
|
*/
|
||||||
|
fun duplicate() {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
methodVisitor.visitInsn(Opcodes.DUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a sequence of instructions for instantiating and throwing an exception based on the provided message.
|
||||||
|
*/
|
||||||
|
fun throwError(message: String) {
|
||||||
|
hasEmittedCustomCode = true
|
||||||
|
new<java.lang.Exception>()
|
||||||
|
methodVisitor.visitInsn(Opcodes.DUP)
|
||||||
|
methodVisitor.visitLdcInsn(message)
|
||||||
|
invokeSpecial<java.lang.Exception>("<init>", "(Ljava/lang/String;)V")
|
||||||
|
methodVisitor.visitInsn(Opcodes.ATHROW)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell the code writer not to emit the default instruction.
|
||||||
|
*/
|
||||||
|
fun preventDefault() {
|
||||||
|
emitDefaultInstruction = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit instruction for invoking a method on the static runtime cost accounting and instrumentation object.
|
||||||
|
*/
|
||||||
|
fun invokeInstrumenter(methodName: String, methodSignature: String) {
|
||||||
|
invokeStatic(RuntimeCostAccounter.TYPE_NAME, methodName, methodSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
djvm/src/main/kotlin/net/corda/djvm/code/Instruction.kt
Normal file
23
djvm/src/main/kotlin/net/corda/djvm/code/Instruction.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
import org.objectweb.asm.Opcodes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte code instruction.
|
||||||
|
*
|
||||||
|
* @property operation The operation code, enumerated in [Opcodes].
|
||||||
|
*/
|
||||||
|
open class Instruction(
|
||||||
|
val operation: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte code for the breakpoint operation.
|
||||||
|
*/
|
||||||
|
const val OP_BREAKPOINT: Int = 202
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.corda.djvm.code
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A member definition provider is a hook for [ClassMutator], from where one can modify the name and meta-data of
|
||||||
|
* processed class members.
|
||||||
|
*/
|
||||||
|
interface MemberDefinitionProvider : DefinitionProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for providing modifications to a member definition.
|
||||||
|
*
|
||||||
|
* @param context The context in which the hook is called.
|
||||||
|
* @param member The original member definition.
|
||||||
|
*
|
||||||
|
* @return The updated member definition, or [member] if no changes are desired.
|
||||||
|
*/
|
||||||
|
fun define(context: AnalysisRuntimeContext, member: Member): Member
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import org.objectweb.asm.Label
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Branch instruction.
|
||||||
|
*
|
||||||
|
* @property label The label of the target.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class BranchInstruction(
|
||||||
|
operation: Int,
|
||||||
|
val label: Label
|
||||||
|
) : Instruction(operation)
|
@ -0,0 +1,12 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import org.objectweb.asm.Label
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label of a code block.
|
||||||
|
*
|
||||||
|
* @property label The label for the given code block.
|
||||||
|
*/
|
||||||
|
class CodeLabel(
|
||||||
|
val label: Label
|
||||||
|
) : NoOperationInstruction()
|
@ -0,0 +1,20 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import org.objectweb.asm.Opcodes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic invocation instruction.
|
||||||
|
*
|
||||||
|
* @property memberName The name of the method to invoke.
|
||||||
|
* @property signature The function signature of the method being invoked.
|
||||||
|
* @property numberOfArguments The number of arguments to pass to the target.
|
||||||
|
* @property returnsValueOrReference False if the target returns `void`, or true if it returns a value or a reference.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class DynamicInvocationInstruction(
|
||||||
|
val memberName: String,
|
||||||
|
val signature: String,
|
||||||
|
val numberOfArguments: Int,
|
||||||
|
val returnsValueOrReference: Boolean
|
||||||
|
) : Instruction(Opcodes.INVOKEDYNAMIC)
|
@ -0,0 +1,13 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instruction with a single, constant integer operand.
|
||||||
|
*
|
||||||
|
* @property operand The integer operand.
|
||||||
|
*/
|
||||||
|
class IntegerInstruction(
|
||||||
|
operation: Int,
|
||||||
|
val operand: Int
|
||||||
|
) : Instruction(operation)
|
@ -0,0 +1,35 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import net.corda.djvm.references.MemberReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field access and method invocation instruction.
|
||||||
|
*
|
||||||
|
* @property owner The class owning the field or method.
|
||||||
|
* @property memberName The name of the field or the method being accessed.
|
||||||
|
* @property signature The return type of a field or function signature for a method.
|
||||||
|
* @property ownerIsInterface If the member is a method, this is true if the owner is an interface.
|
||||||
|
* @property isMethod Indicates whether the member is a method or a field.
|
||||||
|
*/
|
||||||
|
class MemberAccessInstruction(
|
||||||
|
operation: Int,
|
||||||
|
val owner: String,
|
||||||
|
val memberName: String,
|
||||||
|
val signature: String,
|
||||||
|
val ownerIsInterface: Boolean = false,
|
||||||
|
val isMethod: Boolean = false
|
||||||
|
) : Instruction(operation) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The absolute name of the referenced member.
|
||||||
|
*/
|
||||||
|
val reference = "$owner.$memberName:$signature"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a member reference representation of the target of the instruction.
|
||||||
|
*/
|
||||||
|
val member: MemberReference
|
||||||
|
get() = MemberReference(owner, memberName, signature)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import org.objectweb.asm.Opcodes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instruction that, surprise surprise (!), does nothing!
|
||||||
|
*/
|
||||||
|
open class NoOperationInstruction : Instruction(Opcodes.NOP)
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import org.objectweb.asm.Label
|
||||||
|
import org.objectweb.asm.Opcodes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table switch instruction.
|
||||||
|
*
|
||||||
|
* @property min The minimum key value.
|
||||||
|
* @property max The maximum key value.
|
||||||
|
* @property defaultHandler The label of the default handler block.
|
||||||
|
* @property handlers The labels of each of the handler blocks, where the label of the handler block for key
|
||||||
|
* `min + i` is at index `i` in `handlers`.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class TableSwitchInstruction(
|
||||||
|
val min: Int,
|
||||||
|
val max: Int,
|
||||||
|
val defaultHandler: Label,
|
||||||
|
val handlers: List<Label>
|
||||||
|
) : Instruction(Opcodes.TABLESWITCH)
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import org.objectweb.asm.Label
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try-catch block.
|
||||||
|
*
|
||||||
|
* @property typeName The type of the exception being caught.
|
||||||
|
* @property handler The label of the exception handler.
|
||||||
|
*/
|
||||||
|
class TryCatchBlock(
|
||||||
|
val typeName: String,
|
||||||
|
val handler: Label
|
||||||
|
) : NoOperationInstruction()
|
@ -0,0 +1,13 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import org.objectweb.asm.Label
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try-finally block.
|
||||||
|
*
|
||||||
|
* @property handler The handler for the finally-block.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class TryFinallyBlock(
|
||||||
|
val handler: Label
|
||||||
|
) : NoOperationInstruction()
|
@ -0,0 +1,13 @@
|
|||||||
|
package net.corda.djvm.code.instructions
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object instantiation instruction.
|
||||||
|
*
|
||||||
|
* @property typeName The class name of the object being instantiated.
|
||||||
|
*/
|
||||||
|
class TypeInstruction(
|
||||||
|
operation: Int,
|
||||||
|
val typeName: String
|
||||||
|
) : Instruction(operation)
|
30
djvm/src/main/kotlin/net/corda/djvm/costing/RuntimeCost.kt
Normal file
30
djvm/src/main/kotlin/net/corda/djvm/costing/RuntimeCost.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package net.corda.djvm.costing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cost metric to be used in a sandbox environment. The metric has a threshold and a mechanism for reporting violations.
|
||||||
|
* The implementation assumes that each metric is tracked on a per-thread basis, i.e., that each sandbox runs on its own
|
||||||
|
* thread.
|
||||||
|
*
|
||||||
|
* @param threshold The threshold for this metric.
|
||||||
|
* @param errorMessage A delegate for generating an error message based on the thread it was reported from.
|
||||||
|
*/
|
||||||
|
class RuntimeCost(
|
||||||
|
threshold: Long,
|
||||||
|
errorMessage: (Thread) -> String
|
||||||
|
) : TypedRuntimeCost<Long>(
|
||||||
|
0,
|
||||||
|
{ value: Long -> value > threshold },
|
||||||
|
errorMessage
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the accumulated cost by an integer.
|
||||||
|
*/
|
||||||
|
fun increment(incrementBy: Int) = increment(incrementBy.toLong())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the accumulated cost by a long integer.
|
||||||
|
*/
|
||||||
|
fun increment(incrementBy: Long = 1) = incrementAndCheck { value -> Math.addExact(value, incrementBy) }
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package net.corda.djvm.costing
|
||||||
|
|
||||||
|
import net.corda.djvm.execution.ExecutionProfile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides a summary of the accumulated costs for the runtime metrics that are being tracked. It also keeps
|
||||||
|
* track of applicable thresholds and will terminate sandbox execution if any of them are breached.
|
||||||
|
*
|
||||||
|
* The costs are tracked on a per-thread basis, and thus, are isolated for each sandbox. Each sandbox live on its own
|
||||||
|
* thread.
|
||||||
|
*/
|
||||||
|
class RuntimeCostSummary private constructor(
|
||||||
|
allocationCostThreshold: Long,
|
||||||
|
jumpCostThreshold: Long,
|
||||||
|
invocationCostThreshold: Long,
|
||||||
|
throwCostThreshold: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new runtime cost tracker based on an execution profile.
|
||||||
|
*/
|
||||||
|
constructor(profile: ExecutionProfile) : this(
|
||||||
|
allocationCostThreshold = profile.allocationCostThreshold,
|
||||||
|
jumpCostThreshold = profile.jumpCostThreshold,
|
||||||
|
invocationCostThreshold = profile.invocationCostThreshold,
|
||||||
|
throwCostThreshold = profile.throwCostThreshold
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accumulated cost of memory allocations.
|
||||||
|
*/
|
||||||
|
val allocationCost = RuntimeCost(allocationCostThreshold) {
|
||||||
|
"Sandbox [${it.name}] terminated due to over-allocation"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accumulated cost of jump operations.
|
||||||
|
*/
|
||||||
|
val jumpCost = RuntimeCost(jumpCostThreshold) {
|
||||||
|
"Sandbox [${it.name}] terminated due to excessive use of looping"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accumulated cost of method invocations.
|
||||||
|
*/
|
||||||
|
val invocationCost = RuntimeCost(invocationCostThreshold) {
|
||||||
|
"Sandbox [${it.name}] terminated due to excessive method calling"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accumulated cost of throw operations.
|
||||||
|
*/
|
||||||
|
val throwCost = RuntimeCost(throwCostThreshold) {
|
||||||
|
"Sandbox [${it.name}] terminated due to excessive exception throwing"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package net.corda.djvm.costing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a sandbox threshold is violated. This will kill the current thread and consequently exit the
|
||||||
|
* sandbox.
|
||||||
|
*
|
||||||
|
* @property message The description of the condition causing the problem.
|
||||||
|
*/
|
||||||
|
class ThresholdViolationException(
|
||||||
|
override val message: String
|
||||||
|
) : ThreadDeath()
|
@ -0,0 +1,72 @@
|
|||||||
|
package net.corda.djvm.costing
|
||||||
|
|
||||||
|
import net.corda.djvm.utilities.loggerFor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cost metric to be used in a sandbox environment. The metric has a threshold and a mechanism for reporting violations.
|
||||||
|
* The implementation assumes that each metric is tracked on a per-thread basis, i.e., that each sandbox runs on its own
|
||||||
|
* thread.
|
||||||
|
*
|
||||||
|
* @param initialValue The initial value of this metric.
|
||||||
|
* @property thresholdPredicate A delegate for determining whether a threshold has been reached or not.
|
||||||
|
* @property errorMessage A delegate for generating an error message based on the thread it was reported from.
|
||||||
|
*/
|
||||||
|
open class TypedRuntimeCost<T>(
|
||||||
|
initialValue: T,
|
||||||
|
private val thresholdPredicate: (T) -> Boolean,
|
||||||
|
private val errorMessage: (Thread) -> String
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The thread-local container for the cost accumulator.
|
||||||
|
*/
|
||||||
|
private val costValue = object : ThreadLocal<T>() {
|
||||||
|
override fun initialValue() = initialValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property getter for accessing the current accumulated cost.
|
||||||
|
*/
|
||||||
|
val value: T
|
||||||
|
get() = costValue.get()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for doing a guarded increment of the cost value, with a mechanism for consistent error reporting
|
||||||
|
* and nuking of the current thread environment if threshold breaches are encountered.
|
||||||
|
*/
|
||||||
|
protected fun incrementAndCheck(increment: (T) -> T) {
|
||||||
|
val currentThread = getAndCheckThread() ?: return
|
||||||
|
val newValue = increment(costValue.get())
|
||||||
|
costValue.set(newValue)
|
||||||
|
if (thresholdPredicate(newValue)) {
|
||||||
|
val message = errorMessage(currentThread)
|
||||||
|
logger.error("Threshold breached; {}", message)
|
||||||
|
throw ThresholdViolationException(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If [filteredThreads] is specified, this method will filter out those threads whenever threshold constraints are
|
||||||
|
* being tested. This can be used to disable cost accounting on a primary thread, for instance.
|
||||||
|
*/
|
||||||
|
private fun getAndCheckThread(): Thread? {
|
||||||
|
val currentThread = Thread.currentThread()
|
||||||
|
if (filteredThreads.contains(currentThread)) {
|
||||||
|
logger.trace("Thread will not be affected by runtime costing")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return currentThread
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of threads to which cost accounting will be disabled.
|
||||||
|
*/
|
||||||
|
private val filteredThreads: List<Thread> = emptyList()
|
||||||
|
|
||||||
|
private val logger = loggerFor<RuntimeCost>()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
djvm/src/main/kotlin/net/corda/djvm/execution/CostSummary.kt
Normal file
37
djvm/src/main/kotlin/net/corda/djvm/execution/CostSummary.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
import net.corda.djvm.costing.RuntimeCostSummary
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A read-only copy of a the costs accumulated in an [IsolatedTask].
|
||||||
|
*
|
||||||
|
* @property allocations Number of bytes allocated.
|
||||||
|
* @property invocations Number of invocations made.
|
||||||
|
* @property jumps Number of jumps made (includes conditional branches that might not have been taken).
|
||||||
|
* @property throws Number of throws made.
|
||||||
|
*/
|
||||||
|
data class CostSummary(
|
||||||
|
val allocations: Long,
|
||||||
|
val invocations: Long,
|
||||||
|
val jumps: Long,
|
||||||
|
val throws: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a read-only cost summary object from an instance of [RuntimeCostSummary].
|
||||||
|
*/
|
||||||
|
constructor(costs: RuntimeCostSummary) : this(
|
||||||
|
costs.allocationCost.value,
|
||||||
|
costs.invocationCost.value,
|
||||||
|
costs.jumpCost.value,
|
||||||
|
costs.throwCost.value
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* A blank summary of costs.
|
||||||
|
*/
|
||||||
|
val empty = CostSummary(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
import net.corda.djvm.SandboxConfiguration
|
||||||
|
import net.corda.djvm.source.ClassSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The executor is responsible for spinning up a deterministic, sandboxed environment and launching the referenced code
|
||||||
|
* block inside it. The code will run on a separate thread for complete isolation, and to enable context-based costing
|
||||||
|
* of said code. Any exceptions should be forwarded to the caller of [SandboxExecutor.run]. Similarly, the returned
|
||||||
|
* output from the referenced code block should be returned to the caller.
|
||||||
|
*
|
||||||
|
* @param configuration The configuration of the sandbox.
|
||||||
|
*/
|
||||||
|
class DeterministicSandboxExecutor<TInput, TOutput>(
|
||||||
|
configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT
|
||||||
|
) : SandboxExecutor<TInput, TOutput>(configuration) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short-hand for running a [SandboxedRunnable] in a sandbox by its type reference.
|
||||||
|
*/
|
||||||
|
inline fun <reified TRunnable : SandboxedRunnable<TInput, TOutput>> run(input: TInput):
|
||||||
|
ExecutionSummaryWithResult<TOutput?> {
|
||||||
|
return run(ClassSource.fromClassName(TRunnable::class.java.name), input)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functionality runnable by a sandbox executor, marked for discoverability.
|
||||||
|
*/
|
||||||
|
interface DiscoverableRunnable
|
@ -0,0 +1,45 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The execution profile of a [SandboxedRunnable] when run in a sandbox.
|
||||||
|
*
|
||||||
|
* @property allocationCostThreshold The threshold placed on allocations.
|
||||||
|
* @property invocationCostThreshold The threshold placed on invocations.
|
||||||
|
* @property jumpCostThreshold The threshold placed on jumps.
|
||||||
|
* @property throwCostThreshold The threshold placed on throw statements.
|
||||||
|
*/
|
||||||
|
enum class ExecutionProfile(
|
||||||
|
val allocationCostThreshold: Long = Long.MAX_VALUE,
|
||||||
|
val invocationCostThreshold: Long = Long.MAX_VALUE,
|
||||||
|
val jumpCostThreshold: Long = Long.MAX_VALUE,
|
||||||
|
val throwCostThreshold: Long = Long.MAX_VALUE
|
||||||
|
) {
|
||||||
|
|
||||||
|
// TODO Define sensible runtime thresholds and make further improvements to instrumentation.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile with a set of default thresholds.
|
||||||
|
*/
|
||||||
|
DEFAULT(
|
||||||
|
allocationCostThreshold = 1024 * 1024 * 1024,
|
||||||
|
invocationCostThreshold = 1_000_000,
|
||||||
|
jumpCostThreshold = 1_000_000,
|
||||||
|
throwCostThreshold = 1_000_000
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile where no limitations have been imposed on the sandbox.
|
||||||
|
*/
|
||||||
|
UNLIMITED(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile where throw statements have been disallowed.
|
||||||
|
*/
|
||||||
|
DISABLE_THROWS(throwCostThreshold = 0),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile where branching statements have been disallowed.
|
||||||
|
*/
|
||||||
|
DISABLE_BRANCHING(jumpCostThreshold = 0)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
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.
|
||||||
|
*
|
||||||
|
* @property costs The costs accumulated when running the sandboxed code.
|
||||||
|
*/
|
||||||
|
open class ExecutionSummary(
|
||||||
|
val costs: CostSummary = CostSummary.empty
|
||||||
|
)
|
@ -0,0 +1,12 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The summary of the execution of a [SandboxedRunnable] in a sandbox.
|
||||||
|
*
|
||||||
|
* @property result The outcome of the sandboxed operation.
|
||||||
|
* @see ExecutionSummary
|
||||||
|
*/
|
||||||
|
class ExecutionSummaryWithResult<out TResult>(
|
||||||
|
val result: TResult? = null,
|
||||||
|
costs: CostSummary = CostSummary.empty
|
||||||
|
) : ExecutionSummary(costs)
|
105
djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt
Normal file
105
djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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
|
||||||
|
import net.corda.djvm.utilities.loggerFor
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for running a task in an isolated environment.
|
||||||
|
*/
|
||||||
|
class IsolatedTask(
|
||||||
|
private val identifier: String,
|
||||||
|
private val configuration: SandboxConfiguration,
|
||||||
|
private val context: AnalysisContext
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an action in an isolated environment.
|
||||||
|
*/
|
||||||
|
fun <T> run(action: IsolatedTask.() -> T?): Result<T> {
|
||||||
|
val runnable = this
|
||||||
|
val threadName = "DJVM-$identifier-${uniqueIdentifier.getAndIncrement()}"
|
||||||
|
val completionLatch = CountDownLatch(1)
|
||||||
|
var output: T? = null
|
||||||
|
var costs = CostSummary.empty
|
||||||
|
var exception: Throwable? = null
|
||||||
|
thread(name = threadName, isDaemon = true) {
|
||||||
|
logger.trace("Entering isolated runtime environment...")
|
||||||
|
SandboxRuntimeContext(configuration, context.inputClasses).use {
|
||||||
|
output = try {
|
||||||
|
action(runnable)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
logger.error("Exception caught in isolated runtime environment", ex)
|
||||||
|
exception = ex
|
||||||
|
null
|
||||||
|
}
|
||||||
|
costs = CostSummary(
|
||||||
|
runtimeCosts.allocationCost.value,
|
||||||
|
runtimeCosts.invocationCost.value,
|
||||||
|
runtimeCosts.jumpCost.value,
|
||||||
|
runtimeCosts.throwCost.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
logger.trace("Exiting isolated runtime environment...")
|
||||||
|
completionLatch.countDown()
|
||||||
|
}
|
||||||
|
completionLatch.await()
|
||||||
|
val messages = exception.let {
|
||||||
|
when (it) {
|
||||||
|
is SandboxClassLoadingException -> it.messages
|
||||||
|
is SandboxException -> {
|
||||||
|
when (it.exception) {
|
||||||
|
is SandboxClassLoadingException -> it.exception.messages
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} ?: MessageCollection()
|
||||||
|
return Result(threadName, output, costs, messages, exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a run of an [IsolatedTask].
|
||||||
|
*
|
||||||
|
* @property identifier The identifier of the [IsolatedTask].
|
||||||
|
* @property output The result of the run, if successful.
|
||||||
|
* @property costs Captured runtime costs as reported at the end of the run.
|
||||||
|
* @property messages The messages collated during the run.
|
||||||
|
* @property exception This holds any exceptions that might get thrown during execution.
|
||||||
|
*/
|
||||||
|
data class Result<T>(
|
||||||
|
val identifier: String,
|
||||||
|
val output: T?,
|
||||||
|
val costs: CostSummary,
|
||||||
|
val messages: MessageCollection,
|
||||||
|
val exception: Throwable?
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class loader to use for loading the [SandboxedRunnable] and any referenced code in [SandboxExecutor.run].
|
||||||
|
*/
|
||||||
|
val classLoader: SandboxClassLoader
|
||||||
|
get() = SandboxRuntimeContext.instance.classLoader
|
||||||
|
|
||||||
|
// TODO Caching can transcend thread-local contexts by taking the sandbox configuration into account in the key derivation
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An atomically incrementing identifier used to uniquely identify each runnable.
|
||||||
|
*/
|
||||||
|
private val uniqueIdentifier = AtomicLong(0)
|
||||||
|
|
||||||
|
private val logger = loggerFor<IsolatedTask>()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
import net.corda.djvm.utilities.loggerFor
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for processing queued entities.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class QueueProcessor<T>(
|
||||||
|
private val deduplicationKeyExtractor: (T) -> String,
|
||||||
|
vararg elements: T
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val queue = ConcurrentLinkedQueue<T>(elements.toMutableList())
|
||||||
|
|
||||||
|
private val seenElements = mutableSetOf<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an element to the queue.
|
||||||
|
*/
|
||||||
|
fun enqueue(element: T) {
|
||||||
|
logger.trace("Enqueuing {}...", element)
|
||||||
|
val key = deduplicationKeyExtractor(element)
|
||||||
|
if (key !in seenElements) {
|
||||||
|
queue.add(element)
|
||||||
|
seenElements.add(key)
|
||||||
|
} else {
|
||||||
|
logger.trace("Skipped {} as it has already been processed", element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove one element from the queue.
|
||||||
|
*/
|
||||||
|
fun dequeue(): T = queue.remove().apply {
|
||||||
|
logger.trace("Popping {} from the queue...", this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if queue is empty.
|
||||||
|
*/
|
||||||
|
fun isNotEmpty() = queue.isNotEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the current queue with provided action per element.
|
||||||
|
*/
|
||||||
|
inline fun process(action: QueueProcessor<T>.(T) -> Unit) {
|
||||||
|
while (isNotEmpty()) {
|
||||||
|
val element = dequeue()
|
||||||
|
action(this, element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logger = loggerFor<QueueProcessor<T>>()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
import net.corda.djvm.source.ClassSource
|
||||||
|
import java.io.PrintWriter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception raised due to a runtime error inside a sandbox.
|
||||||
|
*
|
||||||
|
* @param message The detailed message describing the problem.
|
||||||
|
* @property sandboxName The name of the sandbox in which the error occurred.
|
||||||
|
* @property sandboxClass The class used as the sandbox entry point.
|
||||||
|
* @property executionSummary A snapshot of the execution summary for the sandbox.
|
||||||
|
* @property exception The inner exception, as it was raised within the sandbox.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class SandboxException(
|
||||||
|
message: String,
|
||||||
|
val sandboxName: String,
|
||||||
|
val sandboxClass: ClassSource,
|
||||||
|
val executionSummary: ExecutionSummary,
|
||||||
|
val exception: Throwable
|
||||||
|
) : Exception(message, exception) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide programmatic access to the stack trace information captured at the time the exception was thrown.
|
||||||
|
*/
|
||||||
|
override fun getStackTrace(): Array<out StackTraceElement>? = exception.stackTrace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the stack trace information of the exception using a [PrintWriter].
|
||||||
|
*/
|
||||||
|
override fun printStackTrace(writer: PrintWriter) = exception.printStackTrace(writer)
|
||||||
|
|
||||||
|
}
|
216
djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt
Normal file
216
djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
package net.corda.djvm.execution
|
||||||
|
|
||||||
|
import net.corda.djvm.SandboxConfiguration
|
||||||
|
import net.corda.djvm.analysis.AnalysisContext
|
||||||
|
import net.corda.djvm.messages.Message
|
||||||
|
import net.corda.djvm.references.ClassReference
|
||||||
|
import net.corda.djvm.references.MemberReference
|
||||||
|
import net.corda.djvm.rewiring.LoadedClass
|
||||||
|
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The executor is responsible for spinning up a sandboxed environment and launching the referenced code block inside
|
||||||
|
* it. Any exceptions should be forwarded to the caller of [SandboxExecutor.run]. Similarly, the returned output from
|
||||||
|
* the referenced code block should be returned to the caller.
|
||||||
|
*
|
||||||
|
* @property configuration The configuration of sandbox.
|
||||||
|
*/
|
||||||
|
open class SandboxExecutor<in TInput, out TOutput>(
|
||||||
|
protected val configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val classModule = configuration.analysisConfiguration.classModule
|
||||||
|
|
||||||
|
private val classResolver = configuration.analysisConfiguration.classResolver
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param runnableClass The entry point of the sandboxed code to run.
|
||||||
|
* @param input The input to provide to the sandboxed environment.
|
||||||
|
*
|
||||||
|
* @returns The output returned from the sandboxed code upon successful completion.
|
||||||
|
* @throws SandboxException Any exception thrown inside the sandbox gets wrapped and re-thrown in the context of the
|
||||||
|
* caller, with additional information about the sandboxed environment.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
open fun run(
|
||||||
|
runnableClass: ClassSource,
|
||||||
|
input: TInput
|
||||||
|
): 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.
|
||||||
|
// 3. Since this is hitting the class loader, we are also remapping and rewriting the classes using the provided
|
||||||
|
// emitters and definition providers.
|
||||||
|
// 4. While traversing and validating, we build up another queue of references inside the reference validator.
|
||||||
|
// 5. We drain this queue by validating class references and member references; this means validating the
|
||||||
|
// existence of these referenced classes and members, and making sure that rule validation has been run on
|
||||||
|
// all reachable code.
|
||||||
|
// 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)
|
||||||
|
// 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 {
|
||||||
|
validate(context, classLoader, classSources)
|
||||||
|
val loadedClass = classLoader.loadClassAndBytes(runnableClass, context)
|
||||||
|
val instance = loadedClass.type.newInstance()
|
||||||
|
val method = loadedClass.type.getMethod("run", Any::class.java)
|
||||||
|
try {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
method.invoke(instance, input) as? TOutput?
|
||||||
|
} catch (ex: InvocationTargetException) {
|
||||||
|
throw ex.targetException
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.trace("Execution of {} with input {} resulted in {}", runnableClass, input, result)
|
||||||
|
when (result.exception) {
|
||||||
|
null -> return ExecutionSummaryWithResult(result.output, result.costs)
|
||||||
|
else -> throw SandboxException(
|
||||||
|
Message.getMessageFromException(result.exception),
|
||||||
|
result.identifier,
|
||||||
|
runnableClass,
|
||||||
|
ExecutionSummary(result.costs),
|
||||||
|
result.exception
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a class source using the sandbox class loader, yielding a [LoadedClass] object with the class' byte code,
|
||||||
|
* type and name attached.
|
||||||
|
*
|
||||||
|
* @param classSource The class source to load.
|
||||||
|
*
|
||||||
|
* @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 {
|
||||||
|
classLoader.loadClassAndBytes(classSource, context)
|
||||||
|
}
|
||||||
|
return result.output ?: throw ClassNotFoundException(classSource.qualifiedClassName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the provided class source(s). This method runs the same validation that takes place in [run], except
|
||||||
|
* from runtime accounting as the entry point(s) will never be executed.
|
||||||
|
*
|
||||||
|
* @param classSources The classes that, together with their dependencies, should be validated.
|
||||||
|
*
|
||||||
|
* @return A collection of loaded classes with their byte code representation for the provided class sources, and a
|
||||||
|
* set of messages produced during validation.
|
||||||
|
* @throws Exception Upon failure, an exception with details about any rule violations and/or invalid references.
|
||||||
|
*/
|
||||||
|
@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 {
|
||||||
|
validate(context, classLoader, classSources.toList())
|
||||||
|
}
|
||||||
|
logger.trace("Validation of {} resulted in {}", classSources, result)
|
||||||
|
when (result.exception) {
|
||||||
|
null -> return result.output!!
|
||||||
|
else -> throw result.exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the provided class source(s) using a pre-defined analysis context.
|
||||||
|
*
|
||||||
|
* @param context The pre-defined analysis context to use during validation.
|
||||||
|
* @param classLoader The class loader to use for validation.
|
||||||
|
* @param classSources The classes that, together with their dependencies, should be validated.
|
||||||
|
*
|
||||||
|
* @return A collection of loaded classes with their byte code representation for the provided class sources, and a
|
||||||
|
* set of messages produced during validation.
|
||||||
|
* @throws Exception Upon failure, an exception with details about any rule violations and/or invalid references.
|
||||||
|
*/
|
||||||
|
private fun validate(
|
||||||
|
context: AnalysisContext, classLoader: SandboxClassLoader, classSources: List<ClassSource>
|
||||||
|
): ReferenceValidationSummary {
|
||||||
|
processClassQueue(*classSources.toTypedArray()) { classSource, className ->
|
||||||
|
val didLoad = try {
|
||||||
|
classLoader.loadClassAndBytes(classSource, context)
|
||||||
|
true
|
||||||
|
} catch (exception: SandboxClassLoadingException) {
|
||||||
|
// Continue; all warnings and errors are captured in [context.messages]
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (didLoad) {
|
||||||
|
context.classes[className]?.apply {
|
||||||
|
context.references.referencesFromLocation(className)
|
||||||
|
.map { it.reference }
|
||||||
|
.filterIsInstance<ClassReference>()
|
||||||
|
.filter { it.className != className }
|
||||||
|
.distinct()
|
||||||
|
.map { ClassSource.fromClassName(it.className, className) }
|
||||||
|
.forEach(::enqueue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a dynamic queue of [ClassSource] entries.
|
||||||
|
*/
|
||||||
|
private inline fun processClassQueue(
|
||||||
|
vararg elements: ClassSource, action: QueueProcessor<ClassSource>.(ClassSource, String) -> Unit
|
||||||
|
) {
|
||||||
|
QueueProcessor({ it.qualifiedClassName }, *elements).process { classSource ->
|
||||||
|
val className = classResolver.reverse(classModule.getBinaryClassName(classSource.qualifiedClassName))
|
||||||
|
if (!whitelist.matches(className)) {
|
||||||
|
action(classSource, className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fail if there are reported errors in the current analysis context.
|
||||||
|
*/
|
||||||
|
private fun failOnReportedErrorsInContext(context: AnalysisContext) {
|
||||||
|
if (context.messages.errorCount > 0) {
|
||||||
|
for (reference in context.references) {
|
||||||
|
for (location in context.references.locationsFromReference(reference)) {
|
||||||
|
val originReference = when {
|
||||||
|
location.memberName.isBlank() -> ClassReference(location.className)
|
||||||
|
else -> MemberReference(location.className, location.memberName, location.signature)
|
||||||
|
}
|
||||||
|
context.recordClassOrigin(reference.className, originReference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SandboxClassLoadingException(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logger = loggerFor<SandboxExecutor<TInput, TOutput>>()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
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?
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package net.corda.djvm.formatting
|
||||||
|
|
||||||
|
import net.corda.djvm.references.ClassModule
|
||||||
|
import net.corda.djvm.references.MemberInformation
|
||||||
|
import net.corda.djvm.references.MemberModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functionality for formatting a member.
|
||||||
|
*/
|
||||||
|
class MemberFormatter(
|
||||||
|
private val classModule: ClassModule = ClassModule(),
|
||||||
|
private val memberModule: MemberModule = MemberModule()
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a member.
|
||||||
|
*/
|
||||||
|
fun format(member: MemberInformation): String {
|
||||||
|
val className = classModule.getFormattedClassName(member.className)
|
||||||
|
val memberName = if (memberModule.isConstructor(member)) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
".${member.memberName}"
|
||||||
|
}
|
||||||
|
return if (memberModule.isField(member)) {
|
||||||
|
"$className$memberName"
|
||||||
|
} else {
|
||||||
|
"$className$memberName(${format(member.signature)})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a member's signature.
|
||||||
|
*/
|
||||||
|
fun format(abbreviatedSignature: String): String {
|
||||||
|
var level = 0
|
||||||
|
val stringBuilder = StringBuilder()
|
||||||
|
for (char in abbreviatedSignature) {
|
||||||
|
if (char == ')') {
|
||||||
|
level -= 1
|
||||||
|
}
|
||||||
|
if (level >= 1) {
|
||||||
|
stringBuilder.append(char)
|
||||||
|
}
|
||||||
|
if (char == '(') {
|
||||||
|
level += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return generateMemberSignature(stringBuilder.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not a signature is for a method.
|
||||||
|
*/
|
||||||
|
fun isMethod(abbreviatedSignature: String): Boolean {
|
||||||
|
return abbreviatedSignature.startsWith("(")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the short representation of a class name.
|
||||||
|
*/
|
||||||
|
fun getShortClassName(fullClassName: String): String {
|
||||||
|
return classModule.getShortName(fullClassName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a prettified version of a native signature.
|
||||||
|
*/
|
||||||
|
private fun generateMemberSignature(abbreviatedSignature: String): String {
|
||||||
|
return classModule.getTypes(abbreviatedSignature).joinToString(", ") {
|
||||||
|
classModule.getShortName(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
53
djvm/src/main/kotlin/net/corda/djvm/messages/Message.kt
Normal file
53
djvm/src/main/kotlin/net/corda/djvm/messages/Message.kt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package net.corda.djvm.messages
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.SourceLocation
|
||||||
|
import net.corda.djvm.execution.SandboxException
|
||||||
|
import net.corda.djvm.rewiring.SandboxClassLoadingException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message of a given severity as it was reported during analysis or validation.
|
||||||
|
*
|
||||||
|
* @property message The message recorded.
|
||||||
|
* @property severity The severity of the message.
|
||||||
|
* @property location The location from which the message was recorded.
|
||||||
|
*/
|
||||||
|
data class Message(
|
||||||
|
val message: String,
|
||||||
|
val severity: Severity,
|
||||||
|
val location: SourceLocation = SourceLocation()
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun toString() = location.toString().let {
|
||||||
|
when {
|
||||||
|
it.isBlank() -> "[${severity.shortName}] $message"
|
||||||
|
else -> "[${severity.shortName}] $it: $message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a message from a [Throwable] with an optional location.
|
||||||
|
*/
|
||||||
|
fun fromThrowable(throwable: Throwable, location: SourceLocation = SourceLocation()): Message {
|
||||||
|
return Message(getMessageFromException(throwable), Severity.ERROR, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a clean description of the provided exception.
|
||||||
|
*/
|
||||||
|
fun getMessageFromException(exception: Throwable): String {
|
||||||
|
val exceptionType = when (exception::class.java.simpleName) {
|
||||||
|
Exception::class.java.simpleName,
|
||||||
|
SandboxClassLoadingException::class.java.simpleName,
|
||||||
|
SandboxException::class.java.simpleName -> null
|
||||||
|
else -> exception::class.java.simpleName.removeSuffix("Exception")
|
||||||
|
}
|
||||||
|
return exception.message?.let { message ->
|
||||||
|
(exceptionType?.let { "$it: " } ?: "") + message
|
||||||
|
} ?: exceptionType ?: "Unknown error"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
package net.corda.djvm.messages
|
||||||
|
|
||||||
|
import net.corda.djvm.references.MemberInformation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of captured problems and messages, grouped by class and member. The collection also handles de-duplication
|
||||||
|
* of entries and keeps track of how many messages have been recorded at each severity level.
|
||||||
|
*
|
||||||
|
* @property minimumSeverity Only record messages of this severity or higher.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||||
|
class MessageCollection(
|
||||||
|
private val minimumSeverity: Severity = Severity.INFORMATIONAL,
|
||||||
|
private val prefixFilters: List<String> = emptyList()
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val seenEntries = mutableSetOf<String>()
|
||||||
|
|
||||||
|
private val classMessages = mutableMapOf<String, MutableList<Message>>()
|
||||||
|
|
||||||
|
private val memberMessages = mutableMapOf<String, MutableList<Message>>()
|
||||||
|
|
||||||
|
private var cachedEntries: List<Message>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a message to the collection.
|
||||||
|
*/
|
||||||
|
fun add(message: Message) {
|
||||||
|
if (message.severity < minimumSeverity) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (prefixFilters.isNotEmpty() && !prefixFilters.any { message.location.className.startsWith(it) }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val location = message.location
|
||||||
|
val key = "${location.className}:${location.memberName}:${location.lineNumber}:${message.message}"
|
||||||
|
if (seenEntries.contains(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenEntries.add(key)
|
||||||
|
when {
|
||||||
|
location.memberName.isBlank() ->
|
||||||
|
messagesFor(location.className).add(message)
|
||||||
|
else ->
|
||||||
|
messagesFor(location.className, location.memberName, location.signature).add(message)
|
||||||
|
}
|
||||||
|
cachedEntries = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a list of messages to the collection.
|
||||||
|
*/
|
||||||
|
fun addAll(messages: List<Message>) {
|
||||||
|
for (message in messages) {
|
||||||
|
add(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all recorded messages for a given class.
|
||||||
|
*/
|
||||||
|
fun messagesFor(className: String) =
|
||||||
|
classMessages.getOrPut(className) { mutableListOf() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all recorded messages for a given class member.
|
||||||
|
*/
|
||||||
|
fun messagesFor(className: String, memberName: String, signature: String) =
|
||||||
|
memberMessages.getOrPut("$className.$memberName:$signature") { mutableListOf() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all recorded messages for a given class or class member.
|
||||||
|
*/
|
||||||
|
fun messagesFor(member: MemberInformation) = when {
|
||||||
|
member.memberName.isBlank() -> messagesFor(member.className)
|
||||||
|
else -> messagesFor(member.className, member.memberName, member.signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the collection of messages is empty.
|
||||||
|
*/
|
||||||
|
fun isEmpty() = count == 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the collection of messages is non-empty.
|
||||||
|
*/
|
||||||
|
fun isNotEmpty() = !isEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a consistently sorted list of messages of severity greater than or equal to [minimumSeverity].
|
||||||
|
*/
|
||||||
|
fun sorted(): List<Message> {
|
||||||
|
val entries = cachedEntries
|
||||||
|
if (entries != null) {
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
cachedEntries = this
|
||||||
|
.all
|
||||||
|
.filter { it.severity >= minimumSeverity }
|
||||||
|
.distinctBy { "${it.severity} ${it.location.className}.${it.location.memberName} ${it.message}" }
|
||||||
|
.sortedWith(compareBy(
|
||||||
|
{ it.severity.precedence },
|
||||||
|
{ it.location.sourceFile },
|
||||||
|
{ it.location.lineNumber },
|
||||||
|
{ "${it.location.className}${it.location.memberName}" }
|
||||||
|
))
|
||||||
|
return cachedEntries!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of messages that have been recorded.
|
||||||
|
*/
|
||||||
|
val count: Int
|
||||||
|
get() = sorted().count()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of errors that have been recorded.
|
||||||
|
*/
|
||||||
|
val errorCount: Int
|
||||||
|
get() = sorted().count { it.severity == Severity.ERROR }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of warnings that have been recorded.
|
||||||
|
*/
|
||||||
|
val warningCount: Int
|
||||||
|
get() = sorted().count { it.severity == Severity.WARNING }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of information messages that have been recorded.
|
||||||
|
*/
|
||||||
|
val infoCount: Int
|
||||||
|
get() = sorted().count { it.severity == Severity.INFORMATIONAL }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of trace messages that have been recorded.
|
||||||
|
*/
|
||||||
|
val traceCount: Int
|
||||||
|
get() = sorted().count { it.severity == Severity.TRACE }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The breakdown of numbers of messages per severity level.
|
||||||
|
*/
|
||||||
|
val statistics: Map<Severity, Int>
|
||||||
|
get() = mapOf(
|
||||||
|
Severity.TRACE to traceCount,
|
||||||
|
Severity.INFORMATIONAL to infoCount,
|
||||||
|
Severity.WARNING to warningCount,
|
||||||
|
Severity.ERROR to errorCount
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all the messages and messages that have been recorded, across all classes and class members.
|
||||||
|
*/
|
||||||
|
private val all: List<Message>
|
||||||
|
get() =
|
||||||
|
mutableListOf<Message>().apply {
|
||||||
|
addAll(classMessages.filter { it.value.isNotEmpty() }.flatMap { it.value })
|
||||||
|
addAll(memberMessages.filter { it.value.isNotEmpty() }.flatMap { it.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
djvm/src/main/kotlin/net/corda/djvm/messages/Severity.kt
Normal file
31
djvm/src/main/kotlin/net/corda/djvm/messages/Severity.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package net.corda.djvm.messages
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The severity of a message.
|
||||||
|
*
|
||||||
|
* @property shortName The short descriptive name of the severity class.
|
||||||
|
* @property precedence The importance of the severity. The lower the number, the higher it is ranked.
|
||||||
|
* @property color The color to use for the severity when printed to a color-enabled terminal.
|
||||||
|
*/
|
||||||
|
enum class Severity(val shortName: String, val precedence: Int, val color: String?) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace message.
|
||||||
|
*/
|
||||||
|
TRACE("TRACE", 3, null),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Informational message.
|
||||||
|
*/
|
||||||
|
INFORMATIONAL("INFO", 2, null),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A warning; something that probably should be fixed, but that does not block from further sandbox execution.
|
||||||
|
*/
|
||||||
|
WARNING("WARN", 1, "yellow"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error; will result in termination of the sandbox execution, if currently active.
|
||||||
|
*/
|
||||||
|
ERROR("ERROR", 0, "red")
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation-specific functionality.
|
||||||
|
*/
|
||||||
|
open class AnnotationModule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any of the annotations marks determinism.
|
||||||
|
*/
|
||||||
|
fun isDeterministic(annotations: Set<String>): Boolean {
|
||||||
|
return annotations.any { isDeterministic(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if annotation is deterministic.
|
||||||
|
*/
|
||||||
|
fun isDeterministic(annotation: String): Boolean {
|
||||||
|
return annotation.endsWith("/deterministic;", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any of the annotations marks non-determinism.
|
||||||
|
*/
|
||||||
|
fun isNonDeterministic(annotations: Set<String>): Boolean {
|
||||||
|
return annotations.any { isNonDeterministic(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if annotation in non-deterministic.
|
||||||
|
*/
|
||||||
|
fun isNonDeterministic(annotation: String): Boolean {
|
||||||
|
return annotation.endsWith("/nondeterministic;", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
119
djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt
Normal file
119
djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
import net.corda.djvm.utilities.loggerFor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a hierarchy of classes.
|
||||||
|
*/
|
||||||
|
class ClassHierarchy(
|
||||||
|
private val classModule: ClassModule = ClassModule(),
|
||||||
|
private val memberModule: MemberModule = MemberModule()
|
||||||
|
) : Iterable<ClassRepresentation> {
|
||||||
|
|
||||||
|
private val classMap = mutableMapOf<String, ClassRepresentation>()
|
||||||
|
|
||||||
|
private val ancestorMap = mutableMapOf<String, List<ClassRepresentation>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add class to the class hierarchy. If the class already exists in the class hierarchy, the existing record will
|
||||||
|
* be overwritten by the new instance.
|
||||||
|
*
|
||||||
|
* @param clazz The class to add to the class hierarchy.
|
||||||
|
*/
|
||||||
|
fun add(clazz: ClassRepresentation) {
|
||||||
|
logger.trace("Adding type {} to hierarchy...", clazz)
|
||||||
|
ancestorMap.clear()
|
||||||
|
classMap[clazz.name] = clazz
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a class from the class hierarchy, if defined.
|
||||||
|
*/
|
||||||
|
operator fun get(name: String) = if (classModule.isArray(name)) {
|
||||||
|
classMap[OBJECT_NAME]
|
||||||
|
} else {
|
||||||
|
classMap[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of all registered class names.
|
||||||
|
*/
|
||||||
|
val names: Set<String>
|
||||||
|
get() = classMap.keys
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of registered classes.
|
||||||
|
*/
|
||||||
|
val size: Int
|
||||||
|
get() = classMap.keys.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get location of class.
|
||||||
|
*/
|
||||||
|
fun location(name: String): String = get(name)?.let {
|
||||||
|
when {
|
||||||
|
it.sourceFile.isNotBlank() -> it.sourceFile
|
||||||
|
else -> it.name
|
||||||
|
}
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a class exists in the class hierarchy.
|
||||||
|
*/
|
||||||
|
operator fun contains(name: String) = classMap.contains(if (classModule.isArray(name)) {
|
||||||
|
OBJECT_NAME
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an iterator for the defined set of classes.
|
||||||
|
*/
|
||||||
|
override fun iterator() = classMap.values.iterator()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the implementation of a member, if defined in specified class or any of its ancestors.
|
||||||
|
*/
|
||||||
|
fun getMember(className: String, memberName: String, signature: String): Member? {
|
||||||
|
if (classModule.isArray(className) && memberName == ARRAY_LENGTH) {
|
||||||
|
// Special instruction to retrieve length of array
|
||||||
|
return Member(0, className, memberName, signature, "")
|
||||||
|
}
|
||||||
|
return findAncestors(get(className)).plus(get(OBJECT_NAME))
|
||||||
|
.asSequence()
|
||||||
|
.filterNotNull()
|
||||||
|
.map { memberModule.getFromClass(it, memberName, signature) }
|
||||||
|
.firstOrNull { it != null }
|
||||||
|
.apply {
|
||||||
|
logger.trace("Getting rooted member for {}.{}:{} yields {}", className, memberName, signature, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all ancestors of a class.
|
||||||
|
*/
|
||||||
|
private fun findAncestors(clazz: ClassRepresentation?): List<ClassRepresentation> {
|
||||||
|
if (clazz == null) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return ancestorMap.getOrPut(clazz.name) {
|
||||||
|
val ancestors = mutableListOf(clazz)
|
||||||
|
if (clazz.superClass.isNotEmpty()) {
|
||||||
|
ancestors.addAll(findAncestors(get(clazz.superClass)))
|
||||||
|
}
|
||||||
|
ancestors.addAll(clazz.interfaces.flatMap { findAncestors(get(it)) })
|
||||||
|
ancestors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
private const val OBJECT_NAME = "java/lang/Object"
|
||||||
|
|
||||||
|
private const val ARRAY_LENGTH = "length"
|
||||||
|
|
||||||
|
private val logger = loggerFor<ClassHierarchy>()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
208
djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt
Normal file
208
djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class-specific functionality.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||||
|
class ClassModule : AnnotationModule() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if class representation is an array.
|
||||||
|
*/
|
||||||
|
fun isArray(className: String): Boolean {
|
||||||
|
return className.startsWith('[')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if class is marked to be deterministic.
|
||||||
|
*/
|
||||||
|
fun isDeterministic(clazz: ClassRepresentation): Boolean {
|
||||||
|
return isDeterministic(clazz.annotations)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if class is marked to be non-deterministic.
|
||||||
|
*/
|
||||||
|
fun isNonDeterministic(clazz: ClassRepresentation): Boolean {
|
||||||
|
return isNonDeterministic(clazz.annotations)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full source location for a class based on the package name and the source file.
|
||||||
|
*/
|
||||||
|
fun getFullSourceLocation(clazz: ClassRepresentation, source: String? = null): String {
|
||||||
|
val sourceFile = source ?: clazz.sourceFile
|
||||||
|
return if ('/' in clazz.name) {
|
||||||
|
"${clazz.name.substring(0, clazz.name.lastIndexOf('/'))}/$sourceFile"
|
||||||
|
} else {
|
||||||
|
sourceFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the binary version of a class name.
|
||||||
|
*/
|
||||||
|
fun getBinaryClassName(name: String) =
|
||||||
|
normalizeClassName(name).replace('.', '/')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the formatted version of a class name.
|
||||||
|
*/
|
||||||
|
fun getFormattedClassName(name: String) =
|
||||||
|
normalizeClassName(name).replace('/', '.')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the short name of a class.
|
||||||
|
*/
|
||||||
|
fun getShortName(name: String): String {
|
||||||
|
val className = getFormattedClassName(name)
|
||||||
|
return if ('.' in className) {
|
||||||
|
className.removeRange(0, className.lastIndexOf('.') + 1)
|
||||||
|
} else {
|
||||||
|
className
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize an abbreviated class name.
|
||||||
|
*/
|
||||||
|
fun normalizeClassName(name: Char): String = normalizeClassName("$name")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize an abbreviated class name.
|
||||||
|
*/
|
||||||
|
fun normalizeClassName(name: String): String = when {
|
||||||
|
name == "V" -> "java/lang/Void"
|
||||||
|
name == "Z" -> "java/lang/Boolean"
|
||||||
|
name == "B" -> "java/lang/Byte"
|
||||||
|
name == "C" -> "java/lang/Character"
|
||||||
|
name == "S" -> "java/lang/Short"
|
||||||
|
name == "I" -> "java/lang/Integer"
|
||||||
|
name == "J" -> "java/lang/Long"
|
||||||
|
name == "F" -> "java/lang/Float"
|
||||||
|
name == "D" -> "java/lang/Double"
|
||||||
|
name.startsWith("L") && name.endsWith(";") ->
|
||||||
|
name.substring(1, name.length - 1)
|
||||||
|
name.startsWith("[") ->
|
||||||
|
normalizeClassName(name.substring(1)) + "[]"
|
||||||
|
else -> name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of types referenced in a signature.
|
||||||
|
*/
|
||||||
|
fun getTypes(abbreviatedSignature: String): List<String> {
|
||||||
|
val types = mutableListOf<String>()
|
||||||
|
var isArray = false
|
||||||
|
var arrayLevel = 0
|
||||||
|
var isLongName = false
|
||||||
|
var longName = StringBuilder()
|
||||||
|
for (char in abbreviatedSignature) {
|
||||||
|
if (char in arrayOf('(', ')')) {
|
||||||
|
continue
|
||||||
|
} else if (char == '[') {
|
||||||
|
isArray = true
|
||||||
|
arrayLevel += 1
|
||||||
|
} else if (char == 'L' && !isLongName) {
|
||||||
|
isLongName = true
|
||||||
|
longName = StringBuilder()
|
||||||
|
} else if (char == ';' && isLongName) {
|
||||||
|
val type = longName.toString()
|
||||||
|
if (isArray) {
|
||||||
|
types.add(type + "[]".repeat(arrayLevel))
|
||||||
|
} else {
|
||||||
|
types.add(type)
|
||||||
|
}
|
||||||
|
isLongName = false
|
||||||
|
isArray = false
|
||||||
|
arrayLevel = 0
|
||||||
|
} else if (!isLongName) {
|
||||||
|
val type = normalizeClassName(char)
|
||||||
|
if (type.isNotBlank()) {
|
||||||
|
if (isArray) {
|
||||||
|
types.add(type + "[]".repeat(arrayLevel))
|
||||||
|
} else {
|
||||||
|
types.add(type)
|
||||||
|
}
|
||||||
|
isLongName = false
|
||||||
|
isArray = false
|
||||||
|
arrayLevel = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
longName.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all classes referenced from set of annotation descriptors.
|
||||||
|
*/
|
||||||
|
fun getClassReferencesFromAnnotations(annotations: Set<String>, derive: Boolean): List<String> {
|
||||||
|
return when {
|
||||||
|
!derive -> emptyList()
|
||||||
|
else -> annotations
|
||||||
|
.flatMap { getClassReferencesFromSignature(it) }
|
||||||
|
.filterOutPrimitiveTypes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all classes referenced from a class definition.
|
||||||
|
*/
|
||||||
|
fun getClassReferencesFromClass(clazz: ClassRepresentation, derive: Boolean): List<String> {
|
||||||
|
val classes = (clazz.interfaces + clazz.superClass).filter(String::isNotBlank) +
|
||||||
|
getClassReferencesFromAnnotations(clazz.annotations, derive) +
|
||||||
|
getClassReferencesFromGenericsSignature(clazz.genericsDetails) +
|
||||||
|
getClassReferencesFromMembers(clazz.members.values, derive)
|
||||||
|
return classes.filterOutPrimitiveTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all classes referenced from a set of member definitions.
|
||||||
|
*/
|
||||||
|
fun getClassReferencesFromMembers(members: Iterable<Member>, derive: Boolean): List<String> {
|
||||||
|
return members
|
||||||
|
.flatMap { getClassReferencesFromMember(it, derive) }
|
||||||
|
.filterOutPrimitiveTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all classes referenced from a member definition.
|
||||||
|
*/
|
||||||
|
fun getClassReferencesFromMember(member: Member, derive: Boolean): List<String> {
|
||||||
|
val classes = getClassReferencesFromSignature(member.signature) +
|
||||||
|
getClassReferencesFromAnnotations(member.annotations, derive) +
|
||||||
|
getClassReferencesFromGenericsSignature(member.genericsDetails)
|
||||||
|
return classes.filterOutPrimitiveTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all classes referenced from a member signature.
|
||||||
|
*/
|
||||||
|
fun getClassReferencesFromSignature(signature: String): List<String> {
|
||||||
|
return getTypes(signature)
|
||||||
|
.filterOutPrimitiveTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all classes referenced from a generics signature.
|
||||||
|
*/
|
||||||
|
fun getClassReferencesFromGenericsSignature(signature: String): List<String> {
|
||||||
|
return getTypes(signature.replace(genericTypeSignatureRegex, ";"))
|
||||||
|
.filter { it.contains("/") }
|
||||||
|
.filterOutPrimitiveTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter out primitive types and clean up array types.
|
||||||
|
*/
|
||||||
|
private fun List<String>.filterOutPrimitiveTypes(): List<String> {
|
||||||
|
return this.map { it.replace("[", "").replace("]", "") }
|
||||||
|
.filter { it.length > 1 }
|
||||||
|
.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val genericTypeSignatureRegex = "[<>:]".toRegex()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to class.
|
||||||
|
*
|
||||||
|
* @property className The class name.
|
||||||
|
*/
|
||||||
|
data class ClassReference(
|
||||||
|
override val className: String
|
||||||
|
) : EntityReference
|
@ -0,0 +1,26 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a class.
|
||||||
|
*
|
||||||
|
* @property apiVersion The target API version for which the class was compiled.
|
||||||
|
* @property access The access flags of the class.
|
||||||
|
* @property name The name of the class.
|
||||||
|
* @property superClass The name of the super-class, if any.
|
||||||
|
* @property interfaces The names of the interfaces implemented by the class.
|
||||||
|
* @property sourceFile The name of the compiled source file, if available.
|
||||||
|
* @property genericsDetails Details about generics used.
|
||||||
|
* @property members The set of fields and methods implemented in the class.
|
||||||
|
* @property annotations The set of annotations applied to the class.
|
||||||
|
*/
|
||||||
|
data class ClassRepresentation(
|
||||||
|
val apiVersion: Int,
|
||||||
|
override val access: Int,
|
||||||
|
val name: String,
|
||||||
|
val superClass: String = "",
|
||||||
|
val interfaces: List<String> = listOf(),
|
||||||
|
var sourceFile: String = "",
|
||||||
|
val genericsDetails: String = "",
|
||||||
|
val members: MutableMap<String, Member> = mutableMapOf(),
|
||||||
|
val annotations: MutableSet<String> = mutableSetOf()
|
||||||
|
) : EntityWithAccessFlag
|
@ -0,0 +1,8 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a class or a class member.
|
||||||
|
*/
|
||||||
|
interface EntityReference {
|
||||||
|
val className: String
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entity in a class hierarchy that is attributed zero or more access flags.
|
||||||
|
*
|
||||||
|
* @property access The access flags of the class.
|
||||||
|
*/
|
||||||
|
interface EntityWithAccessFlag {
|
||||||
|
val access: Int
|
||||||
|
}
|
24
djvm/src/main/kotlin/net/corda/djvm/references/Member.kt
Normal file
24
djvm/src/main/kotlin/net/corda/djvm/references/Member.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a class member.
|
||||||
|
*
|
||||||
|
* @property access The access flags of the member.
|
||||||
|
* @property className The name of the owning class.
|
||||||
|
* @property memberName The name of the member.
|
||||||
|
* @property signature The signature of the member.
|
||||||
|
* @property genericsDetails Details about generics used.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
data class Member(
|
||||||
|
override val access: Int,
|
||||||
|
override val className: String,
|
||||||
|
override val memberName: String,
|
||||||
|
override val signature: String,
|
||||||
|
val genericsDetails: String,
|
||||||
|
val annotations: MutableSet<String> = mutableSetOf(),
|
||||||
|
val exceptions: MutableSet<String> = mutableSetOf(),
|
||||||
|
val value: Any? = null
|
||||||
|
) : MemberInformation, EntityWithAccessFlag
|
@ -0,0 +1,16 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a class member.
|
||||||
|
*
|
||||||
|
* @property className The name of the owning class.
|
||||||
|
* @property memberName The name of the member.
|
||||||
|
* @property signature The signature of the member.
|
||||||
|
* @property reference The absolute name of the referenced member.
|
||||||
|
*/
|
||||||
|
interface MemberInformation {
|
||||||
|
val className: String
|
||||||
|
val memberName: String
|
||||||
|
val signature: String
|
||||||
|
val reference: String get() = "$className.$memberName:$signature"
|
||||||
|
}
|
133
djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt
Normal file
133
djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Member-specific functionality.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
class MemberModule : AnnotationModule() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add member definition to class.
|
||||||
|
*/
|
||||||
|
fun addToClass(clazz: ClassRepresentation, member: Member): Member {
|
||||||
|
clazz.members[getQualifyingIdentifier(member)] = member
|
||||||
|
return member
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get member definition for class. Return `null` if the member does not exist.
|
||||||
|
*/
|
||||||
|
fun getFromClass(clazz: ClassRepresentation, memberName: String, signature: String): Member? {
|
||||||
|
return clazz.members[getQualifyingIdentifier(memberName, signature)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if member is a constructor or a static initialization block.
|
||||||
|
*/
|
||||||
|
fun isConstructor(member: MemberInformation): Boolean {
|
||||||
|
return member.memberName == "<init>" // Instance constructor
|
||||||
|
|| member.memberName == "<clinit>" // Static initialization block
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if member is a field.
|
||||||
|
*/
|
||||||
|
fun isField(member: MemberInformation): Boolean {
|
||||||
|
return !member.signature.startsWith("(")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if member is a method.
|
||||||
|
*/
|
||||||
|
fun isMethod(member: MemberInformation): Boolean {
|
||||||
|
return member.signature.startsWith("(")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if member is marked to be deterministic.
|
||||||
|
*/
|
||||||
|
fun isDeterministic(member: Member): Boolean {
|
||||||
|
return isDeterministic(member.annotations)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if member is marked to be non-deterministic.
|
||||||
|
*/
|
||||||
|
fun isNonDeterministic(member: Member): Boolean {
|
||||||
|
return isNonDeterministic(member.annotations)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of arguments that the member expects, based on its signature.
|
||||||
|
*/
|
||||||
|
fun numberOfArguments(signature: String): Int {
|
||||||
|
var count = 0
|
||||||
|
var level = 0
|
||||||
|
var isLongName = false
|
||||||
|
loop@ for (char in signature) {
|
||||||
|
when {
|
||||||
|
char == '(' -> level += 1
|
||||||
|
char == ')' -> level -= 1
|
||||||
|
char == '[' -> continue@loop
|
||||||
|
!isLongName && char == 'L' -> {
|
||||||
|
if (level == 1) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
isLongName = true
|
||||||
|
}
|
||||||
|
isLongName && char == ';' -> {
|
||||||
|
isLongName = false
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (level == 1 && !isLongName) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a function returns `void` or a value/reference type.
|
||||||
|
*/
|
||||||
|
fun returnsValueOrReference(signature: String): Boolean {
|
||||||
|
return !signature.endsWith(")V")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all classes referenced in a member's signature.
|
||||||
|
*/
|
||||||
|
fun findReferencedClasses(member: MemberInformation): List<String> {
|
||||||
|
val classes = mutableListOf<String>()
|
||||||
|
var longName = StringBuilder()
|
||||||
|
var isLongName = false
|
||||||
|
for (char in member.signature) {
|
||||||
|
if (char == 'L' && !isLongName) {
|
||||||
|
longName = StringBuilder()
|
||||||
|
isLongName = true
|
||||||
|
} else if (char == ';' && isLongName) {
|
||||||
|
classes.add(longName.toString())
|
||||||
|
isLongName = false
|
||||||
|
} else if (isLongName) {
|
||||||
|
longName.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the qualifying identifier of the class member.
|
||||||
|
*/
|
||||||
|
fun getQualifyingIdentifier(memberName: String, signature: String): String {
|
||||||
|
return "$memberName:$signature"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the qualifying identifier of the class member.
|
||||||
|
*/
|
||||||
|
private fun getQualifyingIdentifier(member: MemberInformation): String {
|
||||||
|
return getQualifyingIdentifier(member.memberName, member.signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to class member.
|
||||||
|
*
|
||||||
|
* @property className Class name of the owner.
|
||||||
|
* @property memberName Name of the referenced field or method.
|
||||||
|
* @property signature The signature of the field or method.
|
||||||
|
*/
|
||||||
|
data class MemberReference(
|
||||||
|
override val className: String,
|
||||||
|
override val memberName: String,
|
||||||
|
override val signature: String
|
||||||
|
) : EntityReference, MemberInformation
|
@ -0,0 +1,82 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.SourceLocation
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from member references to all discovered call-sites / field accesses for each reference.
|
||||||
|
*/
|
||||||
|
class ReferenceMap(
|
||||||
|
private val classModule: ClassModule
|
||||||
|
) : Iterable<EntityReference> {
|
||||||
|
|
||||||
|
private val queueOfReferences = ConcurrentLinkedQueue<EntityReference>()
|
||||||
|
|
||||||
|
private val locationsPerReference: MutableMap<EntityReference, MutableSet<SourceLocation>> = hashMapOf()
|
||||||
|
|
||||||
|
private val referencesPerLocation: MutableMap<String, MutableSet<ReferenceWithLocation>> = hashMapOf()
|
||||||
|
|
||||||
|
private var numberOfReferences = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add source location association to a target member.
|
||||||
|
*/
|
||||||
|
fun add(target: EntityReference, location: SourceLocation) {
|
||||||
|
locationsPerReference.getOrPut(target) {
|
||||||
|
queueOfReferences.add(target)
|
||||||
|
numberOfReferences += 1
|
||||||
|
hashSetOf()
|
||||||
|
}.add(location)
|
||||||
|
ReferenceWithLocation(location, target).apply {
|
||||||
|
referencesPerLocation.getOrPut(location.key()) { hashSetOf() }.add(this)
|
||||||
|
if (location.memberName.isNotBlank()) {
|
||||||
|
referencesPerLocation.getOrPut(key(location.className)) { hashSetOf() }.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get call-sites and field access locations associated with a target member.
|
||||||
|
*/
|
||||||
|
fun locationsFromReference(target: EntityReference): Set<SourceLocation> =
|
||||||
|
locationsPerReference.getOrElse(target) { emptySet() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up all references made from a class or a class member.
|
||||||
|
*/
|
||||||
|
fun referencesFromLocation(
|
||||||
|
className: String, memberName: String = "", signature: String = ""
|
||||||
|
): Set<ReferenceWithLocation> {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
override fun iterator(): Iterator<EntityReference> = queueOfReferences.iterator()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over the dynamic collection of references.
|
||||||
|
*/
|
||||||
|
fun process(action: (EntityReference) -> Unit) {
|
||||||
|
while (queueOfReferences.isNotEmpty()) {
|
||||||
|
queueOfReferences.remove().apply(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private fun SourceLocation.key() = key(this.className, this.memberName, this.signature)
|
||||||
|
|
||||||
|
private fun key(className: String, memberName: String = "", signature: String = "") =
|
||||||
|
"$className.$memberName:$signature"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package net.corda.djvm.references
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.SourceLocation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a reference with its original source location.
|
||||||
|
*
|
||||||
|
* @property location The source location from which the reference was made.
|
||||||
|
* @property reference The class or class member that was being referenced.
|
||||||
|
* @property description An optional description of the reference itself or the reason for why the reference was
|
||||||
|
* created.
|
||||||
|
*/
|
||||||
|
data class ReferenceWithLocation(
|
||||||
|
val location: SourceLocation,
|
||||||
|
val reference: EntityReference,
|
||||||
|
val description: String = ""
|
||||||
|
)
|
12
djvm/src/main/kotlin/net/corda/djvm/rewiring/ByteCode.kt
Normal file
12
djvm/src/main/kotlin/net/corda/djvm/rewiring/ByteCode.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package net.corda.djvm.rewiring
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byte code representation of a class.
|
||||||
|
*
|
||||||
|
* @property bytes The raw bytes of the class.
|
||||||
|
* @property isModified Indication of whether the class has been modified as part of loading.
|
||||||
|
*/
|
||||||
|
class ByteCode(
|
||||||
|
val bytes: ByteArray,
|
||||||
|
val isModified: Boolean
|
||||||
|
)
|
@ -0,0 +1,48 @@
|
|||||||
|
package net.corda.djvm.rewiring
|
||||||
|
|
||||||
|
import net.corda.djvm.SandboxConfiguration
|
||||||
|
import net.corda.djvm.analysis.AnalysisContext
|
||||||
|
import net.corda.djvm.code.ClassMutator
|
||||||
|
import net.corda.djvm.utilities.loggerFor
|
||||||
|
import org.objectweb.asm.ClassReader
|
||||||
|
import org.objectweb.asm.commons.ClassRemapper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functionality for rewrite parts of a class as it is being loaded.
|
||||||
|
*
|
||||||
|
* @property configuration The configuration of the sandbox.
|
||||||
|
* @property classLoader The class loader used to load the classes that are to be rewritten.
|
||||||
|
* @property remapper A sandbox-aware remapper for inspecting and correcting type names and descriptors.
|
||||||
|
*/
|
||||||
|
open class ClassRewriter(
|
||||||
|
private val configuration: SandboxConfiguration,
|
||||||
|
private val classLoader: ClassLoader,
|
||||||
|
private val remapper: SandboxRemapper = SandboxRemapper(configuration.analysisConfiguration.classResolver)
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process class and allow user to rewrite parts/all of its content through provided hooks.
|
||||||
|
*
|
||||||
|
* @param reader The reader providing the byte code for the desired class.
|
||||||
|
* @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)
|
||||||
|
val writer = SandboxClassWriter(reader, classLoader)
|
||||||
|
val classRemapper = ClassRemapper(writer, remapper)
|
||||||
|
val visitor = ClassMutator(
|
||||||
|
classRemapper,
|
||||||
|
configuration.analysisConfiguration,
|
||||||
|
configuration.definitionProviders,
|
||||||
|
configuration.emitters
|
||||||
|
)
|
||||||
|
visitor.analyze(reader, context, options = ClassReader.EXPAND_FRAMES)
|
||||||
|
val hasBeenModified = visitor.hasBeenModified
|
||||||
|
return ByteCode(writer.toByteArray(), hasBeenModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val logger = loggerFor<ClassRewriter>()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt
Normal file
25
djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package net.corda.djvm.rewiring
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class or interface running in a Java application, together with its raw byte code representation and all references
|
||||||
|
* made from within the type.
|
||||||
|
*
|
||||||
|
* @property type The class/interface representation.
|
||||||
|
* @property byteCode The raw byte code forming the class/interface.
|
||||||
|
*/
|
||||||
|
class LoadedClass(
|
||||||
|
val type: Class<*>,
|
||||||
|
val byteCode: ByteCode
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the loaded type.
|
||||||
|
*/
|
||||||
|
val name: String
|
||||||
|
get() = type.name.replace('.', '/')
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Class(type=$name, size=${byteCode.bytes.size}, isModified=${byteCode.isModified})"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
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.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.
|
||||||
|
* @property context The context in which analysis and processing is performed.
|
||||||
|
*/
|
||||||
|
class SandboxClassLoader(
|
||||||
|
val configuration: SandboxConfiguration,
|
||||||
|
val context: AnalysisContext
|
||||||
|
) : ClassLoader() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The analyzer used to traverse the class hierarchy.
|
||||||
|
*/
|
||||||
|
val analyzer: ClassAndMemberVisitor
|
||||||
|
get() = ruleValidator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of classes that should be left untouched due to pinning.
|
||||||
|
*/
|
||||||
|
private val pinnedClasses = configuration.analysisConfiguration.pinnedClasses
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of classes that should be left untouched due to whitelisting.
|
||||||
|
*/
|
||||||
|
private val whitelistedClasses = configuration.analysisConfiguration.whitelist
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of loaded classes.
|
||||||
|
*/
|
||||||
|
private val loadedClasses = mutableMapOf<String, LoadedClass>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class loader used to find classes on the extended class path.
|
||||||
|
*/
|
||||||
|
private val supportingClassLoader = SourceClassLoader(
|
||||||
|
configuration.analysisConfiguration.classPath,
|
||||||
|
configuration.analysisConfiguration.classResolver
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The re-writer to use for registered classes.
|
||||||
|
*/
|
||||||
|
private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the class with the specified binary name.
|
||||||
|
*
|
||||||
|
* @param name The binary name of the class.
|
||||||
|
* @param resolve If `true` then resolve the class.
|
||||||
|
*
|
||||||
|
* @return The resulting <tt>Class</tt> object.
|
||||||
|
*/
|
||||||
|
override fun loadClass(name: String, resolve: Boolean): Class<*> {
|
||||||
|
return loadClassAndBytes(ClassSource.fromClassName(name), context).type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the class with the specified binary name.
|
||||||
|
*
|
||||||
|
* @param source The class source, including the binary name of the class.
|
||||||
|
* @param context The context in which the analysis is conducted.
|
||||||
|
*
|
||||||
|
* @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)
|
||||||
|
|
||||||
|
// Check if the class has already been loaded.
|
||||||
|
val loadedClass = loadedClasses[name]
|
||||||
|
if (loadedClass != null) {
|
||||||
|
logger.trace("Class {} already loaded", source.qualifiedClassName)
|
||||||
|
return loadedClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the byte code for the specified class.
|
||||||
|
val reader = supportingClassLoader.classReader(name, context, source.origin)
|
||||||
|
|
||||||
|
// Analyse the class if not matching the whitelist.
|
||||||
|
val readClassName = reader.className
|
||||||
|
if (!configuration.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('.', '/')
|
||||||
|
if (qualifiedName in pinnedClasses) {
|
||||||
|
logger.trace("Class {} is marked as pinned", source.qualifiedClassName)
|
||||||
|
val pinnedClasses = LoadedClass(
|
||||||
|
supportingClassLoader.loadClass(name),
|
||||||
|
ByteCode(ByteArray(0), false)
|
||||||
|
)
|
||||||
|
loadedClasses[name] = pinnedClasses
|
||||||
|
if (source.origin != null) {
|
||||||
|
context.recordClassOrigin(name, ClassReference(source.origin))
|
||||||
|
}
|
||||||
|
return pinnedClasses
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any errors were found during analysis.
|
||||||
|
if (context.messages.errorCount > 0) {
|
||||||
|
logger.trace("Errors detected after analyzing class {}", source.qualifiedClassName)
|
||||||
|
throw SandboxClassLoadingException(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the class definition and byte code in accordance with provided rules.
|
||||||
|
val byteCode = rewriter.rewrite(reader, context)
|
||||||
|
|
||||||
|
// Try to define the transformed class.
|
||||||
|
val clazz = try {
|
||||||
|
when {
|
||||||
|
whitelistedClasses.matches(qualifiedName) -> supportingClassLoader.loadClass(name)
|
||||||
|
else -> defineClass(resolvedName, byteCode.bytes, 0, byteCode.bytes.size)
|
||||||
|
}
|
||||||
|
} catch (exception: SecurityException) {
|
||||||
|
throw SecurityException("Cannot redefine class '$resolvedName'", exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache transformed class.
|
||||||
|
val classWithByteCode = LoadedClass(clazz, byteCode)
|
||||||
|
loadedClasses[name] = classWithByteCode
|
||||||
|
if (source.origin != null) {
|
||||||
|
context.recordClassOrigin(name, ClassReference(source.origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("Loaded class {}, bytes={}, isModified={}",
|
||||||
|
source.qualifiedClassName, byteCode.bytes.size, byteCode.isModified)
|
||||||
|
|
||||||
|
return classWithByteCode
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val logger = loggerFor<SandboxClassLoader>()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package net.corda.djvm.rewiring
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisContext
|
||||||
|
import net.corda.djvm.messages.Message
|
||||||
|
import net.corda.djvm.messages.MessageCollection
|
||||||
|
import net.corda.djvm.references.ClassHierarchy
|
||||||
|
import net.corda.djvm.references.EntityReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception raised if the sandbox class loader for some reason fails to load one or more classes.
|
||||||
|
*
|
||||||
|
* @property messages A collection of the problem(s) that caused the class loading to fail.
|
||||||
|
* @property classes The class hierarchy at the time the exception was raised.
|
||||||
|
* @property context The context in which the analysis took place.
|
||||||
|
* @property classOrigins Map of class origins. The resulting set represents the types referencing the class in question.
|
||||||
|
*/
|
||||||
|
class SandboxClassLoadingException(
|
||||||
|
private val context: AnalysisContext,
|
||||||
|
val messages: MessageCollection = context.messages,
|
||||||
|
val classes: ClassHierarchy = context.classes,
|
||||||
|
val classOrigins: Map<String, Set<EntityReference>> = context.classOrigins
|
||||||
|
) : Exception("Failed to load class") {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detailed description of the exception.
|
||||||
|
*/
|
||||||
|
override val message: String?
|
||||||
|
get() = StringBuilder().apply {
|
||||||
|
appendln(super.message)
|
||||||
|
for (message in messages.sorted().map(Message::toString).distinct()) {
|
||||||
|
appendln(" - $message")
|
||||||
|
}
|
||||||
|
}.toString().trimEnd('\r', '\n')
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package net.corda.djvm.rewiring
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader
|
||||||
|
import org.objectweb.asm.ClassWriter
|
||||||
|
import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES
|
||||||
|
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class writer for sandbox execution, with configurable a [classLoader] to ensure correct deduction of the used class
|
||||||
|
* hierarchy.
|
||||||
|
*
|
||||||
|
* @param classReader The [ClassReader] used to read the original class. It will be used to copy the entire constant
|
||||||
|
* pool and bootstrap methods from the original class and also to copy other fragments of original byte code where
|
||||||
|
* applicable.
|
||||||
|
* @property classLoader The class loader used to load the classes that are to be rewritten.
|
||||||
|
* @param flags Option flags that can be used to modify the default behaviour of this class. Must be zero or a
|
||||||
|
* combination of [COMPUTE_MAXS] and [COMPUTE_FRAMES]. These option flags do not affect methods that are copied as is
|
||||||
|
* in the new class. This means that neither the maximum stack size nor the stack frames will be computed for these
|
||||||
|
* methods.
|
||||||
|
*/
|
||||||
|
open class SandboxClassWriter(
|
||||||
|
classReader: ClassReader,
|
||||||
|
private val classLoader: ClassLoader,
|
||||||
|
flags: Int = COMPUTE_FRAMES or COMPUTE_MAXS
|
||||||
|
) : ClassWriter(classReader, flags) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
when {
|
||||||
|
type1 == OBJECT_NAME -> return type1
|
||||||
|
type2 == OBJECT_NAME -> return type2
|
||||||
|
}
|
||||||
|
val class1 = try {
|
||||||
|
classLoader.loadClass(type1.replace('/', '.'))
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
throw TypeNotPresentException(type1, exception)
|
||||||
|
}
|
||||||
|
val class2 = try {
|
||||||
|
classLoader.loadClass(type2.replace('/', '.'))
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
throw TypeNotPresentException(type2, exception)
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
class1.isAssignableFrom(class2) -> type1
|
||||||
|
class2.isAssignableFrom(class1) -> type2
|
||||||
|
class1.isInterface || class2.isInterface -> OBJECT_NAME
|
||||||
|
else -> {
|
||||||
|
var clazz = class1
|
||||||
|
do {
|
||||||
|
clazz = clazz.superclass
|
||||||
|
} while (!clazz.isAssignableFrom(class2))
|
||||||
|
clazz.name.replace('.', '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val OBJECT_NAME = "java/lang/Object"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package net.corda.djvm.rewiring
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.ClassResolver
|
||||||
|
import org.objectweb.asm.commons.Remapper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class name and descriptor re-mapper for use in a sandbox.
|
||||||
|
*
|
||||||
|
* @property classResolver Functionality for resolving the class name of a sandboxed or sandboxable class.
|
||||||
|
*/
|
||||||
|
open class SandboxRemapper(
|
||||||
|
private val classResolver: ClassResolver
|
||||||
|
) : Remapper() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The underlying mapping function for descriptors.
|
||||||
|
*/
|
||||||
|
override fun mapDesc(desc: String): String {
|
||||||
|
return rewriteDescriptor(super.mapDesc(desc))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The underlying mapping function for type names.
|
||||||
|
*/
|
||||||
|
override fun map(typename: String): String {
|
||||||
|
return rewriteTypeName(super.map(typename))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for rewriting a descriptor.
|
||||||
|
*/
|
||||||
|
protected open fun rewriteDescriptor(descriptor: String) =
|
||||||
|
classResolver.resolveDescriptor(descriptor)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for rewriting a type name.
|
||||||
|
*/
|
||||||
|
protected open fun rewriteTypeName(name: String) =
|
||||||
|
classResolver.resolve(name)
|
||||||
|
|
||||||
|
}
|
28
djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt
Normal file
28
djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.djvm.rules
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.validation.RuleContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a rule that applies to class definitions.
|
||||||
|
*/
|
||||||
|
abstract class ClassRule : Rule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a class definition is visited.
|
||||||
|
*
|
||||||
|
* @param context The context in which the rule is to be validated.
|
||||||
|
* @param clazz The class to apply and validate this rule against.
|
||||||
|
*/
|
||||||
|
abstract fun validate(context: RuleContext, clazz: ClassRepresentation)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt
Normal file
28
djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.djvm.rules
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.validation.RuleContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a rule that applies to byte code instructions.
|
||||||
|
*/
|
||||||
|
abstract class InstructionRule : Rule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an instruction is visited.
|
||||||
|
*
|
||||||
|
* @param context The context in which the rule is to be validated.
|
||||||
|
* @param instruction The instruction to apply and validate this rule against.
|
||||||
|
*/
|
||||||
|
abstract fun validate(context: RuleContext, instruction: Instruction)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt
Normal file
28
djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.djvm.rules
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.validation.RuleContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a rule that applies to member definitions.
|
||||||
|
*/
|
||||||
|
abstract class MemberRule : Rule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a member definition is visited.
|
||||||
|
*
|
||||||
|
* @param context The context in which the rule is to be validated.
|
||||||
|
* @param member The class member to apply and validate this rule against.
|
||||||
|
*/
|
||||||
|
abstract fun validate(context: RuleContext, member: Member)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
djvm/src/main/kotlin/net/corda/djvm/rules/Rule.kt
Normal file
23
djvm/src/main/kotlin/net/corda/djvm/rules/Rule.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package net.corda.djvm.rules
|
||||||
|
|
||||||
|
import net.corda.djvm.code.Instruction
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.validation.RuleContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a rule.
|
||||||
|
*/
|
||||||
|
interface Rule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate class, member and/or instruction in the provided context.
|
||||||
|
*
|
||||||
|
* @param context The context in which the rule is to be validated.
|
||||||
|
* @param clazz The class to apply and validate this rule against, if any.
|
||||||
|
* @param member The class member to apply and validate this rule against, if any.
|
||||||
|
* @param instruction The instruction to apply and validate this rule against, if any.
|
||||||
|
*/
|
||||||
|
fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package net.corda.djvm.rules.implementation
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||||
|
import net.corda.djvm.code.ClassDefinitionProvider
|
||||||
|
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.code.instructions.TypeInstruction
|
||||||
|
import net.corda.djvm.references.ClassRepresentation
|
||||||
|
import org.objectweb.asm.Opcodes
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition provider that ensures that all objects inherit from a sandboxed version of [java.lang.Object], with a
|
||||||
|
* deterministic `hashCode()` method.
|
||||||
|
*/
|
||||||
|
class AlwaysInheritFromSandboxedObject : ClassDefinitionProvider, Emitter {
|
||||||
|
|
||||||
|
override fun define(context: AnalysisRuntimeContext, clazz: ClassRepresentation) = when {
|
||||||
|
isDirectSubClassOfObject(context.clazz) -> clazz.copy(superClass = SANDBOX_OBJECT_NAME)
|
||||||
|
else -> clazz
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||||
|
if (instruction is TypeInstruction &&
|
||||||
|
instruction.typeName == OBJECT_NAME) {
|
||||||
|
// When creating new objects, make sure the sandboxed type gets used.
|
||||||
|
new(SANDBOX_OBJECT_NAME, instruction.operation)
|
||||||
|
preventDefault()
|
||||||
|
}
|
||||||
|
if (instruction is MemberAccessInstruction &&
|
||||||
|
instruction.operation == Opcodes.INVOKESPECIAL &&
|
||||||
|
instruction.owner == OBJECT_NAME &&
|
||||||
|
instruction.memberName == CONSTRUCTOR_NAME &&
|
||||||
|
context.clazz.name != SANDBOX_OBJECT_NAME) {
|
||||||
|
// Rewrite object initialisation call so that the sandboxed constructor gets used instead.
|
||||||
|
invokeSpecial(SANDBOX_OBJECT_NAME, CONSTRUCTOR_NAME, "()V", instruction.ownerIsInterface)
|
||||||
|
preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDirectSubClassOfObject(clazz: ClassRepresentation): Boolean {
|
||||||
|
// Check if the super class is java.lang.Object and that current class is not sandbox.java.lang.Object.
|
||||||
|
val isClass = !Modifier.isInterface(clazz.access)
|
||||||
|
return isClass && isObject(clazz.superClass) && clazz.name != SANDBOX_OBJECT_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isObject(superClass: String) = superClass.isBlank() || superClass == OBJECT_NAME
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val OBJECT_NAME = "java/lang/Object"
|
||||||
|
|
||||||
|
private const val SANDBOX_OBJECT_NAME = "sandbox/java/lang/Object"
|
||||||
|
|
||||||
|
private const val CONSTRUCTOR_NAME = "<init>"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 org.objectweb.asm.Opcodes.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use exact integer and long arithmetic where possible.
|
||||||
|
*
|
||||||
|
* Note: Strictly speaking, this rule is not a requirement for determinism, but we believe it helps make code more
|
||||||
|
* robust. The outcome of enabling this rule is that arithmetical overflows for addition and multiplication operations
|
||||||
|
* will be thrown instead of silenced.
|
||||||
|
*/
|
||||||
|
class AlwaysUseExactMath : Emitter {
|
||||||
|
|
||||||
|
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||||
|
if (context.clazz.name == "java/lang/Math") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (instruction.operation) {
|
||||||
|
IADD -> {
|
||||||
|
invokeStatic("java/lang/Math", "addExact", "(II)I")
|
||||||
|
preventDefault()
|
||||||
|
}
|
||||||
|
LADD -> {
|
||||||
|
invokeStatic("java/lang/Math", "addExact", "(JJ)J")
|
||||||
|
preventDefault()
|
||||||
|
}
|
||||||
|
IMUL -> {
|
||||||
|
invokeStatic("java/lang/Math", "multiplyExact", "(II)I")
|
||||||
|
preventDefault()
|
||||||
|
}
|
||||||
|
LMUL -> {
|
||||||
|
invokeStatic("java/lang/Math", "multiplyExact", "(JJ)J")
|
||||||
|
preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO Add mappings for other operations, e.g., increment, negate, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package net.corda.djvm.rules.implementation
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||||
|
import net.corda.djvm.code.MemberDefinitionProvider
|
||||||
|
import net.corda.djvm.references.EntityWithAccessFlag
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.rules.MemberRule
|
||||||
|
import net.corda.djvm.validation.RuleContext
|
||||||
|
import org.objectweb.asm.Opcodes.ACC_SYNCHRONIZED
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition provider that ensures that all methods are non-synchronized in the sandbox.
|
||||||
|
*/
|
||||||
|
class AlwaysUseNonSynchronizedMethods : MemberRule(), MemberDefinitionProvider {
|
||||||
|
|
||||||
|
override fun validate(context: RuleContext, member: Member) = context.validate {
|
||||||
|
if (isConcrete(context.clazz)) {
|
||||||
|
trace("Synchronization specifier will be ignored") given ((member.access and ACC_SYNCHRONIZED) == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun define(context: AnalysisRuntimeContext, member: Member) = when {
|
||||||
|
isConcrete(context.clazz) -> member.copy(access = member.access and ACC_SYNCHRONIZED.inv())
|
||||||
|
else -> member
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isConcrete(entity: EntityWithAccessFlag) = !Modifier.isAbstract(entity.access)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package net.corda.djvm.rules.implementation
|
||||||
|
|
||||||
|
import net.corda.djvm.analysis.AnalysisRuntimeContext
|
||||||
|
import net.corda.djvm.code.MemberDefinitionProvider
|
||||||
|
import net.corda.djvm.references.EntityWithAccessFlag
|
||||||
|
import net.corda.djvm.references.Member
|
||||||
|
import net.corda.djvm.rules.MemberRule
|
||||||
|
import net.corda.djvm.validation.RuleContext
|
||||||
|
import org.objectweb.asm.Opcodes.ACC_STRICT
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition provider that ensures that all methods use strict floating-point arithmetic in the sandbox.
|
||||||
|
*
|
||||||
|
* Note: Future JVM releases may make this pass obsolete; https://bugs.openjdk.java.net/browse/JDK-8175916.
|
||||||
|
*/
|
||||||
|
class AlwaysUseStrictFloatingPointArithmetic : MemberRule(), MemberDefinitionProvider {
|
||||||
|
|
||||||
|
override fun validate(context: RuleContext, member: Member) = context.validate {
|
||||||
|
if (isConcrete(context.clazz)) {
|
||||||
|
trace("Strict floating-point arithmetic will be applied") given ((member.access and ACC_STRICT) == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun define(context: AnalysisRuntimeContext, member: Member) = when {
|
||||||
|
isConcrete(context.clazz) -> member.copy(access = member.access or ACC_STRICT)
|
||||||
|
else -> member
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isConcrete(entity: EntityWithAccessFlag) = !Modifier.isAbstract(entity.access)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
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.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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rule that checks for attempted catches of [ThreadDeath], [ThresholdViolationException], [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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun emit(context: EmitterContext, instruction: Instruction) = context.emit {
|
||||||
|
if (instruction is TryCatchBlock && instruction.typeName in disallowedExceptionTypes) {
|
||||||
|
handlers.add(instruction.handler)
|
||||||
|
} else if (instruction is CodeLabel && isExceptionHandler(instruction.label)) {
|
||||||
|
duplicate()
|
||||||
|
invokeInstrumenter("checkCatch", "(Ljava/lang/Throwable;)V")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val handlers = mutableSetOf<Label>()
|
||||||
|
|
||||||
|
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(
|
||||||
|
"java/lang/Throwable",
|
||||||
|
"java/lang/Error"
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user