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:
Tommy Lillehagen 2018-08-22 16:01:39 +01:00 committed by GitHub
parent 30d07bc998
commit d2ef16cbfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
217 changed files with 9817 additions and 9381 deletions

4
.idea/compiler.xml generated
View File

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

@ -0,0 +1,3 @@
tmp/
*.log
*.log.gz

89
djvm/build.gradle Normal file
View 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
View 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"
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1 @@
djvm_completion

20
djvm/shell/djvm Executable file
View 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
View 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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,6 @@
package net.corda.djvm.execution
/**
* Functionality runnable by a sandbox executor, marked for discoverability.
*/
interface DiscoverableRunnable

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package net.corda.djvm.references
/**
* Reference to a class or a class member.
*/
interface EntityReference {
val className: String
}

View File

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

View 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

View File

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

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

View File

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

View File

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

View File

@ -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 = ""
)

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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