From d2ef16cbfdd7fc1cf5f053bf99df671fcb5fb891 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Wed, 22 Aug 2018 16:01:39 +0100 Subject: [PATCH] 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) --- .idea/compiler.xml | 4 +- djvm/.gitignore | 3 + djvm/build.gradle | 89 + djvm/cli/build.gradle | 45 + .../net/corda/djvm/tools/cli/BuildCommand.kt | 42 + .../net/corda/djvm/tools/cli/CheckCommand.kt | 34 + .../net/corda/djvm/tools/cli/ClassCommand.kt | 216 + .../net/corda/djvm/tools/cli/CommandBase.kt | 265 + .../net/corda/djvm/tools/cli/Commands.kt | 30 + .../corda/djvm/tools/cli/InspectionCommand.kt | 89 + .../net/corda/djvm/tools/cli/NewCommand.kt | 76 + .../net/corda/djvm/tools/cli/Program.kt | 12 + .../net/corda/djvm/tools/cli/RunCommand.kt | 37 + .../net/corda/djvm/tools/cli/ShowCommand.kt | 56 + .../net/corda/djvm/tools/cli/TreeCommand.kt | 34 + .../corda/djvm/tools/cli/VersionProvider.kt | 14 + .../corda/djvm/tools/cli/WhitelistCommand.kt | 14 + .../tools/cli/WhitelistGenerateCommand.kt | 91 + .../djvm/tools/cli/WhitelistShowCommand.kt | 33 + djvm/shell/.gitignore | 1 + djvm/shell/djvm | 20 + djvm/shell/install | 17 + .../net/corda/djvm/SandboxConfiguration.kt | 60 + .../net/corda/djvm/SandboxRuntimeContext.kt | 62 + .../djvm/analysis/AnalysisConfiguration.kt | 55 + .../corda/djvm/analysis/AnalysisContext.kt | 61 + .../djvm/analysis/AnalysisRuntimeContext.kt | 22 + .../djvm/analysis/ClassAndMemberVisitor.kt | 548 ++ .../net/corda/djvm/analysis/ClassResolver.kt | 128 + .../net/corda/djvm/analysis/PrefixTree.kt | 38 + .../net/corda/djvm/analysis/SourceLocation.kt | 89 + .../net/corda/djvm/analysis/Whitelist.kt | 205 + .../djvm/annotations/NonDeterministic.kt | 17 + .../djvm/code/ClassDefinitionProvider.kt | 22 + .../net/corda/djvm/code/ClassMutator.kt | 98 + .../net/corda/djvm/code/DefinitionProvider.kt | 7 + .../kotlin/net/corda/djvm/code/Emitter.kt | 26 + .../net/corda/djvm/code/EmitterContext.kt | 76 + .../net/corda/djvm/code/EmitterModule.kt | 125 + .../kotlin/net/corda/djvm/code/Instruction.kt | 23 + .../djvm/code/MemberDefinitionProvider.kt | 22 + .../code/instructions/BranchInstruction.kt | 15 + .../corda/djvm/code/instructions/CodeLabel.kt | 12 + .../DynamicInvocationInstruction.kt | 20 + .../code/instructions/IntegerInstruction.kt | 13 + .../instructions/MemberAccessInstruction.kt | 35 + .../instructions/NoOperationInstruction.kt | 9 + .../instructions/TableSwitchInstruction.kt | 22 + .../djvm/code/instructions/TryCatchBlock.kt | 14 + .../djvm/code/instructions/TryFinallyBlock.kt | 13 + .../djvm/code/instructions/TypeInstruction.kt | 13 + .../net/corda/djvm/costing/RuntimeCost.kt | 30 + .../corda/djvm/costing/RuntimeCostSummary.kt | 57 + .../costing/ThresholdViolationException.kt | 11 + .../corda/djvm/costing/TypedRuntimeCost.kt | 72 + .../net/corda/djvm/execution/CostSummary.kt | 37 + .../execution/DeterministicSandboxExecutor.kt | 26 + .../djvm/execution/DiscoverableRunnable.kt | 6 + .../corda/djvm/execution/ExecutionProfile.kt | 45 + .../corda/djvm/execution/ExecutionSummary.kt | 12 + .../execution/ExecutionSummaryWithResult.kt | 12 + .../net/corda/djvm/execution/IsolatedTask.kt | 105 + .../corda/djvm/execution/QueueProcessor.kt | 57 + .../corda/djvm/execution/SandboxException.kt | 34 + .../corda/djvm/execution/SandboxExecutor.kt | 216 + .../corda/djvm/execution/SandboxedRunnable.kt | 19 + .../corda/djvm/formatting/MemberFormatter.kt | 75 + .../kotlin/net/corda/djvm/messages/Message.kt | 53 + .../corda/djvm/messages/MessageCollection.kt | 163 + .../net/corda/djvm/messages/Severity.kt | 31 + .../corda/djvm/references/AnnotationModule.kt | 36 + .../corda/djvm/references/ClassHierarchy.kt | 119 + .../net/corda/djvm/references/ClassModule.kt | 208 + .../corda/djvm/references/ClassReference.kt | 10 + .../djvm/references/ClassRepresentation.kt | 26 + .../corda/djvm/references/EntityReference.kt | 8 + .../djvm/references/EntityWithAccessFlag.kt | 10 + .../net/corda/djvm/references/Member.kt | 24 + .../djvm/references/MemberInformation.kt | 16 + .../net/corda/djvm/references/MemberModule.kt | 133 + .../corda/djvm/references/MemberReference.kt | 14 + .../net/corda/djvm/references/ReferenceMap.kt | 82 + .../djvm/references/ReferenceWithLocation.kt | 17 + .../net/corda/djvm/rewiring/ByteCode.kt | 12 + .../net/corda/djvm/rewiring/ClassRewriter.kt | 48 + .../net/corda/djvm/rewiring/LoadedClass.kt | 25 + .../corda/djvm/rewiring/SandboxClassLoader.kt | 159 + .../rewiring/SandboxClassLoadingException.kt | 35 + .../corda/djvm/rewiring/SandboxClassWriter.kt | 66 + .../corda/djvm/rewiring/SandboxRemapper.kt | 41 + .../kotlin/net/corda/djvm/rules/ClassRule.kt | 28 + .../net/corda/djvm/rules/InstructionRule.kt | 28 + .../kotlin/net/corda/djvm/rules/MemberRule.kt | 28 + .../main/kotlin/net/corda/djvm/rules/Rule.kt | 23 + .../AlwaysInheritFromSandboxedObject.kt | 61 + .../implementation/AlwaysUseExactMath.kt | 42 + .../AlwaysUseNonSynchronizedMethods.kt | 30 + .../AlwaysUseStrictFloatingPointArithmetic.kt | 32 + .../implementation/DisallowBreakpoints.kt | 17 + .../DisallowCatchingBlacklistedExceptions.kt | 62 + .../DisallowDynamicInvocation.kt | 18 + .../DisallowFinalizerMethods.kt | 17 + .../implementation/DisallowNativeMethods.kt | 17 + .../DisallowOverriddenSandboxPackage.kt | 17 + .../implementation/DisallowReflection.kt | 30 + .../DisallowUnsupportedApiVersions.kt | 31 + .../IgnoreSynchronizedBlocks.kt | 32 + .../instrumentation/TraceAllocations.kt | 46 + .../instrumentation/TraceInvocations.kt | 23 + .../instrumentation/TraceJumps.kt | 23 + .../instrumentation/TraceThrows.kt | 23 + .../net/corda/djvm/source/ClassSource.kt | 43 + .../djvm/source/JarInputStreamIterator.kt | 62 + .../net/corda/djvm/source/PathClassSource.kt | 35 + .../corda/djvm/source/SourceClassLoader.kt | 110 + .../kotlin/net/corda/djvm/tools/Utilities.kt | 114 + .../net/corda/djvm/utilities/Discovery.kt | 30 + .../net/corda/djvm/utilities/Logging.kt | 10 + .../net/corda/djvm/utilities/Processor.kt | 30 + .../djvm/validation/ConstraintProvider.kt | 68 + .../net/corda/djvm/validation/Reason.kt | 46 + .../validation/ReferenceValidationSummary.kt | 18 + .../djvm/validation/ReferenceValidator.kt | 219 + .../net/corda/djvm/validation/RuleContext.kt | 70 + .../corda/djvm/validation/RuleValidator.kt | 80 + .../main/kotlin/sandbox/java/lang/Object.kt | 19 + .../main/kotlin/sandbox/java/lang/System.kt | 99 + .../djvm/costing/RuntimeCostAccounter.kt | 111 + .../main/resources/jdk8-deterministic.dat.gz | Bin 0 -> 219390 bytes .../resources/kotlin-deterministic.dat.gz | Bin 0 -> 174469 bytes djvm/src/main/resources/log4j2.xml | 39 + djvm/src/test/kotlin/foo/bar/sandbox/A.kt | 9 + djvm/src/test/kotlin/foo/bar/sandbox/B.kt | 10 + djvm/src/test/kotlin/foo/bar/sandbox/C.kt | 11 + .../test/kotlin/foo/bar/sandbox/Callable.kt | 13 + djvm/src/test/kotlin/foo/bar/sandbox/Empty.kt | 5 + .../kotlin/foo/bar/sandbox/KotlinClass.kt | 12 + .../test/kotlin/foo/bar/sandbox/MyObject.kt | 3 + .../kotlin/foo/bar/sandbox/StrictFloat.kt | 10 + .../test/kotlin/net/corda/djvm/TestBase.kt | 169 + .../analysis/ClassAndMemberVisitorTest.kt | 211 + .../corda/djvm/analysis/ClassResolverTest.kt | 50 + .../djvm/analysis/ReferenceValidatorTest.kt | 66 + .../corda/djvm/analysis/SourceLocationTest.kt | 40 + .../net/corda/djvm/analysis/WhitelistTest.kt | 44 + .../djvm/assertions/AssertionExtensions.kt | 71 + .../assertions/AssertiveClassHierarchy.kt | 24 + .../AssertiveClassHierarchyWithClass.kt | 47 + ...sertiveClassHierarchyWithClassAndMember.kt | 28 + .../assertions/AssertiveClassWithByteCode.kt | 27 + .../djvm/assertions/AssertiveMessages.kt | 68 + .../djvm/assertions/AssertiveReferenceMap.kt | 46 + .../AssertiveReferenceMapWithEntity.kt | 21 + .../assertions/AssertiveRuntimeCostSummary.kt | 58 + .../net/corda/djvm/code/ClassMutatorTest.kt | 76 + .../net/corda/djvm/code/EmitterModuleTest.kt | 47 + .../net/corda/djvm/costing/RuntimeCostTest.kt | 31 + .../djvm/execution/SandboxExecutorTest.kt | 310 ++ .../djvm/formatter/MemberFormatterTest.kt | 93 + .../djvm/references/ClassHierarchyTest.kt | 196 + .../corda/djvm/references/ClassModuleTest.kt | 68 + .../corda/djvm/references/MemberModuleTest.kt | 143 + .../corda/djvm/rewiring/ClassRewriterTest.kt | 64 + .../djvm/rules/ReferenceExtractorTest.kt | 62 + .../net/corda/djvm/rules/RuleValidatorTest.kt | 68 + .../djvm/source/SourceClassLoaderTest.kt | 105 + .../net/corda/djvm/utilities/DiscoveryTest.kt | 33 + .../sandbox/greymalkin/StringReturner.kt | 8 + .../test/resources/jar-with-single-class.jar | Bin 0 -> 805 bytes .../test/resources/jar-with-two-classes.jar | Bin 0 -> 1208 bytes djvm/src/test/resources/log4j2.xml | 39 + docs/source/key-concepts-djvm.rst | 376 ++ docs/source/key-concepts.rst | 1 + docs/source/resources/djvm-overview.png | Bin 0 -> 206077 bytes experimental/sandbox/README.md | 22 - experimental/sandbox/build.gradle | 39 - .../net/corda/sandbox/CandidacyStatus.java | 215 - .../net/corda/sandbox/CandidateMethod.java | 110 - .../sandbox/SandboxAwareClassWriter.java | 82 - .../net/corda/sandbox/SandboxRemapper.java | 19 - .../main/java/net/corda/sandbox/Utils.java | 197 - .../corda/sandbox/WhitelistClassLoader.java | 358 -- .../WhitelistClassloadingException.java | 31 - .../net/corda/sandbox/costing/Contract.java | 48 - .../sandbox/costing/ContractExecutor.java | 24 - .../sandbox/costing/RuntimeCostAccounter.java | 157 - .../corda/sandbox/tools/SandboxCreator.java | 127 - .../CostInstrumentingMethodVisitor.java | 165 - .../DefinitelyDisallowedMethodVisitor.java | 15 - .../sandbox/visitors/SandboxPathVisitor.java | 57 - .../WhitelistCheckingClassVisitor.java | 187 - .../WhitelistCheckingMethodVisitor.java | 172 - .../sandbox/costing/RuntimeCostAccounter.java | 32 - .../main/resources/java.base.deterministic | 3 - .../src/main/resources/java.base.disallowed | 1 - .../src/main/resources/java.base.hand-picked | 9 - .../resources/java8.interfaces_for_compat | 2 - .../src/main/resources/java8.scan.java.lang | 1039 ---- .../java8.scan.java.lang_and_reflect | 1210 ----- .../resources/java8.scan.java.lang_and_util | 4429 ----------------- .../sandbox/src/main/resources/logback.xml | 21 - .../corda/sandbox/CandidateMethodTest.java | 38 - .../java/net/corda/sandbox/Constants.java | 11 - .../java/net/corda/sandbox/TestUtils.java | 159 - .../sandbox/WhitelistClassLoaderTest.java | 161 - .../DeterministicClassInstrumenterTest.java | 74 - .../costing/SandboxedRewritingTest.java | 126 - .../sandbox/greymalkin/StringReturner.java | 10 - .../test/resources/java/lang/Exception.class | Bin 653 -> 0 bytes .../test/resources/java/lang/Throwable.class | Bin 8298 -> 0 bytes .../resources/java/lang/reflect/Array.class | Bin 1859 -> 0 bytes .../test/resources/java/util/ArrayList.class | Bin 11009 -> 0 bytes .../src/test/resources/java/util/Arrays.class | Bin 35976 -> 0 bytes .../src/test/resources/logback-test.xml | 29 - .../sandbox/src/test/resources/resource.jar | Bin 1679853 -> 0 bytes .../sandbox/src/test/resources/sandbox.jar | Bin 3705466 -> 0 bytes settings.gradle | 3 +- 217 files changed, 9817 insertions(+), 9381 deletions(-) create mode 100644 djvm/.gitignore create mode 100644 djvm/build.gradle create mode 100644 djvm/cli/build.gradle create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CheckCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CommandBase.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Commands.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Program.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ShowCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/VersionProvider.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt create mode 100644 djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistShowCommand.kt create mode 100644 djvm/shell/.gitignore create mode 100755 djvm/shell/djvm create mode 100755 djvm/shell/install create mode 100644 djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisRuntimeContext.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/SourceLocation.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/annotations/NonDeterministic.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/ClassDefinitionProvider.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/DefinitionProvider.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/EmitterContext.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/Instruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/MemberDefinitionProvider.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/BranchInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/CodeLabel.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/DynamicInvocationInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/IntegerInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/MemberAccessInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/NoOperationInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/TableSwitchInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/code/instructions/TypeInstruction.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/costing/RuntimeCost.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/costing/RuntimeCostSummary.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/costing/ThresholdViolationException.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/CostSummary.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/DiscoverableRunnable.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/QueueProcessor.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/SandboxException.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/execution/SandboxedRunnable.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/messages/Message.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/messages/MessageCollection.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/messages/Severity.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/AnnotationModule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/ClassReference.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/ClassRepresentation.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/EntityReference.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/EntityWithAccessFlag.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/Member.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/MemberReference.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/references/ReferenceWithLocation.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/ByteCode.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoadingException.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxRemapper.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/Rule.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/AlwaysInheritFromSandboxedObject.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/AlwaysUseExactMath.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/AlwaysUseNonSynchronizedMethods.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/AlwaysUseStrictFloatingPointArithmetic.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowBreakpoints.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowDynamicInvocation.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowFinalizerMethods.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNativeMethods.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowOverriddenSandboxPackage.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowReflection.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowUnsupportedApiVersions.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/source/JarInputStreamIterator.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/source/PathClassSource.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/utilities/Logging.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/utilities/Processor.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/validation/ConstraintProvider.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/validation/Reason.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidationSummary.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidator.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/validation/RuleContext.kt create mode 100644 djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt create mode 100644 djvm/src/main/kotlin/sandbox/java/lang/Object.kt create mode 100644 djvm/src/main/kotlin/sandbox/java/lang/System.kt create mode 100644 djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt create mode 100644 djvm/src/main/resources/jdk8-deterministic.dat.gz create mode 100644 djvm/src/main/resources/kotlin-deterministic.dat.gz create mode 100644 djvm/src/main/resources/log4j2.xml create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/A.kt create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/B.kt create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/C.kt create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/Callable.kt create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/Empty.kt create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/KotlinClass.kt create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/MyObject.kt create mode 100644 djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/TestBase.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/analysis/ClassResolverTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/analysis/ReferenceValidatorTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/analysis/SourceLocationTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/analysis/WhitelistTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertionExtensions.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassHierarchy.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassHierarchyWithClass.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassHierarchyWithClassAndMember.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveMessages.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMapWithEntity.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveRuntimeCostSummary.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/formatter/MemberFormatterTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/references/ClassHierarchyTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/references/ClassModuleTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt create mode 100644 djvm/src/test/kotlin/net/corda/djvm/utilities/DiscoveryTest.kt create mode 100644 djvm/src/test/kotlin/sandbox/greymalkin/StringReturner.kt create mode 100644 djvm/src/test/resources/jar-with-single-class.jar create mode 100644 djvm/src/test/resources/jar-with-two-classes.jar create mode 100644 djvm/src/test/resources/log4j2.xml create mode 100644 docs/source/key-concepts-djvm.rst create mode 100644 docs/source/resources/djvm-overview.png delete mode 100644 experimental/sandbox/README.md delete mode 100644 experimental/sandbox/build.gradle delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/CandidacyStatus.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/CandidateMethod.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/SandboxAwareClassWriter.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/SandboxRemapper.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/Utils.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassLoader.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/WhitelistClassloadingException.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/costing/Contract.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/costing/ContractExecutor.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/costing/RuntimeCostAccounter.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/tools/SandboxCreator.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/visitors/CostInstrumentingMethodVisitor.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/visitors/DefinitelyDisallowedMethodVisitor.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/visitors/SandboxPathVisitor.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingClassVisitor.java delete mode 100644 experimental/sandbox/src/main/java/net/corda/sandbox/visitors/WhitelistCheckingMethodVisitor.java delete mode 100644 experimental/sandbox/src/main/java/sandbox/net/corda/sandbox/costing/RuntimeCostAccounter.java delete mode 100644 experimental/sandbox/src/main/resources/java.base.deterministic delete mode 100644 experimental/sandbox/src/main/resources/java.base.disallowed delete mode 100644 experimental/sandbox/src/main/resources/java.base.hand-picked delete mode 100644 experimental/sandbox/src/main/resources/java8.interfaces_for_compat delete mode 100644 experimental/sandbox/src/main/resources/java8.scan.java.lang delete mode 100644 experimental/sandbox/src/main/resources/java8.scan.java.lang_and_reflect delete mode 100644 experimental/sandbox/src/main/resources/java8.scan.java.lang_and_util delete mode 100644 experimental/sandbox/src/main/resources/logback.xml delete mode 100644 experimental/sandbox/src/test/java/net/corda/sandbox/CandidateMethodTest.java delete mode 100644 experimental/sandbox/src/test/java/net/corda/sandbox/Constants.java delete mode 100644 experimental/sandbox/src/test/java/net/corda/sandbox/TestUtils.java delete mode 100644 experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java delete mode 100644 experimental/sandbox/src/test/java/net/corda/sandbox/costing/DeterministicClassInstrumenterTest.java delete mode 100644 experimental/sandbox/src/test/java/net/corda/sandbox/costing/SandboxedRewritingTest.java delete mode 100644 experimental/sandbox/src/test/java/sandbox/greymalkin/StringReturner.java delete mode 100644 experimental/sandbox/src/test/resources/java/lang/Exception.class delete mode 100644 experimental/sandbox/src/test/resources/java/lang/Throwable.class delete mode 100644 experimental/sandbox/src/test/resources/java/lang/reflect/Array.class delete mode 100644 experimental/sandbox/src/test/resources/java/util/ArrayList.class delete mode 100644 experimental/sandbox/src/test/resources/java/util/Arrays.class delete mode 100644 experimental/sandbox/src/test/resources/logback-test.xml delete mode 100644 experimental/sandbox/src/test/resources/resource.jar delete mode 100644 experimental/sandbox/src/test/resources/sandbox.jar diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1c4c966a8b..a27c575da8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -71,6 +71,8 @@ + + @@ -229,4 +231,4 @@ - \ No newline at end of file + diff --git a/djvm/.gitignore b/djvm/.gitignore new file mode 100644 index 0000000000..9aaddce91f --- /dev/null +++ b/djvm/.gitignore @@ -0,0 +1,3 @@ +tmp/ +*.log +*.log.gz diff --git a/djvm/build.gradle b/djvm/build.gradle new file mode 100644 index 0000000000..04947e10e5 --- /dev/null +++ b/djvm/build.gradle @@ -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 + } + } +} diff --git a/djvm/cli/build.gradle b/djvm/cli/build.gradle new file mode 100644 index 0000000000..01a2324d5b --- /dev/null +++ b/djvm/cli/build.gradle @@ -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" +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt new file mode 100644 index 0000000000..298ebcb1ce --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt @@ -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 = 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 + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CheckCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CheckCommand.kt new file mode 100644 index 0000000000..6c6e27ea08 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CheckCommand.kt @@ -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 + get() = classes + + @Parameters(description = ["The partial or fully qualified names of the Java classes to analyse and validate."]) + var classes: Array = emptyArray() + + override fun printSuccess(classes: List>) { + for (clazz in classes.sortedBy { it.name }) { + printVerbose("Class ${clazz.name} validated") + } + printVerbose() + } + + override fun processClasses(classes: List>) { + val sources = classes.map { ClassSource.fromClassName(it.name) } + val summary = executor.validate(*sources.toTypedArray()) + printMessages(summary.messages, summary.classOrigins) + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt new file mode 100644 index 0000000000..e3538535d0 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt @@ -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 = 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 = emptyArray() + + abstract val filters: Array + + private val classModule = ClassModule() + + private lateinit var classLoader: ClassLoader + + protected var executor = SandboxExecutor() + + private var derivedWhitelist: Whitelist = Whitelist.MINIMAL + + abstract fun processClasses(classes: List>) + + open fun printSuccess(classes: List>) {} + + 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): List> { + return findDiscoverableRunnables(filters) + findReferencedClasses(filters) + findClassesInJars(filters) + } + + private fun findDiscoverableRunnables(filters: Array): List> { + val classes = find() + 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): List> { + 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): List> { + return filters.filter { isJarFile(it) }.flatMap { jarFile -> + mutableListOf>().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 Boolean.emptyListIfTrueOtherwiseNull(): List? = when (this) { + true -> emptyList() + false -> null + } + +} \ No newline at end of file diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CommandBase.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CommandBase.kt new file mode 100644 index 0000000000..d4a3c4afb8 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/CommandBase.kt @@ -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 { + + @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 { + 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> = 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" + } + } + } + +} \ No newline at end of file diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Commands.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Commands.kt new file mode 100644 index 0000000000..65dd4c9ed4 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Commands.kt @@ -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) = when (CommandLine.call(this, System.err, *args)) { + true -> 0 + else -> 1 + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt new file mode 100644 index 0000000000..f6d779ceb2 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt @@ -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 + get() = classes + + @Parameters(description = ["The partial or fully qualified names of the Java classes to inspect."]) + var classes: Array = emptyArray() + + override fun processClasses(classes: List>) { + 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) + } + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt new file mode 100644 index 0000000000..e2df2f4d0f --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt @@ -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 = 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() + + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Program.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Program.kt new file mode 100644 index 0000000000..3c4524b0f3 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Program.kt @@ -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) { + exitProcess(Commands().run(args)) +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt new file mode 100644 index 0000000000..3f4fd93108 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt @@ -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 + get() = classes + + @Parameters(description = ["The partial or fully qualified names of the Java classes to run."]) + var classes: Array = emptyArray() + + override fun processClasses(classes: List>) { + 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) + } + } + } + +} \ No newline at end of file diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ShowCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ShowCommand.kt new file mode 100644 index 0000000000..67a6deac68 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ShowCommand.kt @@ -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 + get() = classes + + @Parameters(description = ["The partial or fully qualified names of the Java classes to inspect."]) + var classes: Array = emptyArray() + + override fun processClasses(classes: List>) { + 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() + } + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt new file mode 100644 index 0000000000..47b24ffa44 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt @@ -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 + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/VersionProvider.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/VersionProvider.kt new file mode 100644 index 0000000000..24c26e2094 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/VersionProvider.kt @@ -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 = arrayOf( + Manifests.read("Corda-Release-Version") + ) +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistCommand.kt new file mode 100644 index 0000000000..7671f9c246 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistCommand.kt @@ -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() diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt new file mode 100644 index 0000000000..4882c9355f --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt @@ -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 = 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() + 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) { + for (entry in entries.sorted().distinct()) { + stream.println(entry) + } + } + +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistShowCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistShowCommand.kt new file mode 100644 index 0000000000..85ec05fdc1 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistShowCommand.kt @@ -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 = 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 + } + +} diff --git a/djvm/shell/.gitignore b/djvm/shell/.gitignore new file mode 100644 index 0000000000..4bc7c1cffa --- /dev/null +++ b/djvm/shell/.gitignore @@ -0,0 +1 @@ +djvm_completion diff --git a/djvm/shell/djvm b/djvm/shell/djvm new file mode 100755 index 0000000000..5388332d2f --- /dev/null +++ b/djvm/shell/djvm @@ -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 "$@" diff --git a/djvm/shell/install b/djvm/shell/install new file mode 100755 index 0000000000..4874c00583 --- /dev/null +++ b/djvm/shell/install @@ -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 diff --git a/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt new file mode 100644 index 0000000000..da7cd0d553 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt @@ -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, + val emitters: List, + val definitionProviders: List, + 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 = Discovery.find(), + emitters: List? = null, + definitionProviders: List = 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 + ) + } +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt b/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt new file mode 100644 index 0000000000..49013b9ba3 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt @@ -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 +) { + + /** + * 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() { + 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) + } + + } + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt new file mode 100644 index 0000000000..4114aa32af --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt @@ -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 = emptySet(), + val minimumSeverityLevel: Severity = Severity.WARNING, + val classPath: List = emptyList(), + val analyzeAnnotations: Boolean = false, + val prefixFilters: List = 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 = 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 + } + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt new file mode 100644 index 0000000000..c37beab616 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt @@ -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 +) { + + private val origins = mutableMapOf>() + + /** + * 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> + get() = origins + + companion object { + + /** + * Create a new analysis context from provided configuration. + */ + fun fromConfiguration(configuration: AnalysisConfiguration, classes: List): 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("/", ".") + + } + +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisRuntimeContext.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisRuntimeContext.kt new file mode 100644 index 0000000000..5853748272 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisRuntimeContext.kt @@ -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 +) diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt new file mode 100644 index 0000000000..53a934cd80 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt @@ -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 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? + ) { + 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? + ): 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 } + } + + } + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt new file mode 100644 index 0000000000..e2d23b3f70 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt @@ -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, + 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() + } + +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt new file mode 100644 index 0000000000..a063965b76 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt @@ -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 = 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 + } + +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/SourceLocation.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/SourceLocation.kt new file mode 100644 index 0000000000..78ea440123 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/SourceLocation.kt @@ -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() + + } + +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt new file mode 100644 index 0000000000..95d8c2ff39 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt @@ -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, + private val textEntries: Set +) { + + /** + * Set of seen names that matched with the whitelist. + */ + private val seenNames = mutableSetOf() + + /** + * 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): 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 + 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() + val entries = mutableSetOf() + 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() + } + +} + diff --git a/djvm/src/main/kotlin/net/corda/djvm/annotations/NonDeterministic.kt b/djvm/src/main/kotlin/net/corda/djvm/annotations/NonDeterministic.kt new file mode 100644 index 0000000000..eab08ecf75 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/annotations/NonDeterministic.kt @@ -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 \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassDefinitionProvider.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassDefinitionProvider.kt new file mode 100644 index 0000000000..cd99d1b3b1 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassDefinitionProvider.kt @@ -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 + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt new file mode 100644 index 0000000000..b8d2fa8a93 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt @@ -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 = emptyList(), + private val emitters: List = 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(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(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(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(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() + } + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/DefinitionProvider.kt b/djvm/src/main/kotlin/net/corda/djvm/code/DefinitionProvider.kt new file mode 100644 index 0000000000..d284fddef5 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/DefinitionProvider.kt @@ -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 \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt new file mode 100644 index 0000000000..7d953a28e1 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt @@ -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 + +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterContext.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterContext.kt new file mode 100644 index 0000000000..31ce3d7f8a --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterContext.kt @@ -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) + } + +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt new file mode 100644 index 0000000000..8d0f25bd02 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt @@ -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 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 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() + methodVisitor.visitInsn(Opcodes.DUP) + methodVisitor.visitLdcInsn(message) + invokeSpecial("", "(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) + } + +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Instruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Instruction.kt new file mode 100644 index 0000000000..90cee5daad --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Instruction.kt @@ -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 + + } + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/MemberDefinitionProvider.kt b/djvm/src/main/kotlin/net/corda/djvm/code/MemberDefinitionProvider.kt new file mode 100644 index 0000000000..b6a2433db8 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/MemberDefinitionProvider.kt @@ -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 + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/BranchInstruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/BranchInstruction.kt new file mode 100644 index 0000000000..92e060b923 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/BranchInstruction.kt @@ -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) \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/CodeLabel.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/CodeLabel.kt new file mode 100644 index 0000000000..73243e9721 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/CodeLabel.kt @@ -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() diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/DynamicInvocationInstruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/DynamicInvocationInstruction.kt new file mode 100644 index 0000000000..2dbbff45c4 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/DynamicInvocationInstruction.kt @@ -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) diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/IntegerInstruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/IntegerInstruction.kt new file mode 100644 index 0000000000..ac7581acbe --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/IntegerInstruction.kt @@ -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) \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/MemberAccessInstruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/MemberAccessInstruction.kt new file mode 100644 index 0000000000..da0d761f18 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/MemberAccessInstruction.kt @@ -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) + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/NoOperationInstruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/NoOperationInstruction.kt new file mode 100644 index 0000000000..e27c951e29 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/NoOperationInstruction.kt @@ -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) \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TableSwitchInstruction.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TableSwitchInstruction.kt new file mode 100644 index 0000000000..b3fec2bd4e --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TableSwitchInstruction.kt @@ -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