mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +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="demobench_main" 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_source_example-code_integrationTest" target="1.8" />
|
||||
<module name="docs_source_example-code_main" target="1.8" />
|
||||
@ -229,4 +231,4 @@
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
|
||||
</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…
Reference in New Issue
Block a user