Skip to content

Commit 3368a73

Browse files
committed
Add SecurityManager support to block suspicious code #622
1 parent dcf607b commit 3368a73

File tree

7 files changed

+189
-8
lines changed

7 files changed

+189
-8
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/UtExecutionResult.kt

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ data class UtOverflowFailure(
1717
override val exception: Throwable,
1818
) : UtExecutionFailure()
1919

20+
data class UtSandboxFailure(
21+
override val exception: Throwable
22+
) : UtExecutionFailure()
23+
2024
/**
2125
* unexpectedFail (when exceptions such as NPE, IOBE, etc. appear, but not thrown by a user, applies both for function under test and nested calls )
2226
* expectedCheckedThrow (when function under test or nested call explicitly says that checked exception could be thrown and throws it)

utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt

+7-6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import kotlin.math.max
7171
import kotlin.math.min
7272
import kotlinx.collections.immutable.persistentListOf
7373
import kotlinx.collections.immutable.persistentSetOf
74+
import org.utbot.framework.plugin.api.UtSandboxFailure
7475
import soot.ArrayType
7576
import soot.BooleanType
7677
import soot.ByteType
@@ -88,6 +89,7 @@ import soot.SootField
8889
import soot.Type
8990
import soot.VoidType
9091
import sun.java2d.cmm.lcms.LcmsServiceProvider
92+
import java.security.AccessControlException
9193

9294
// hack
9395
const val MAX_LIST_SIZE = 10
@@ -371,12 +373,11 @@ class Resolver(
371373
return if (explicit) {
372374
UtExplicitlyThrownException(exception, inNestedMethod)
373375
} else {
374-
// TODO SAT-1561
375-
val isOverflow = exception is ArithmeticException && exception.message?.contains("overflow") == true
376-
if (isOverflow) {
377-
UtOverflowFailure(exception)
378-
} else {
379-
UtImplicitlyThrownException(exception, inNestedMethod)
376+
when {
377+
// TODO SAT-1561
378+
exception is ArithmeticException && exception.message?.contains("overflow") == true -> UtOverflowFailure(exception)
379+
exception is AccessControlException -> UtSandboxFailure(exception)
380+
else -> UtImplicitlyThrownException(exception, inNestedMethod)
380381
}
381382
}
382383
}

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt

+14
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation
113113
import org.utbot.framework.plugin.api.UtNullModel
114114
import org.utbot.framework.plugin.api.UtPrimitiveModel
115115
import org.utbot.framework.plugin.api.UtReferenceModel
116+
import org.utbot.framework.plugin.api.UtSandboxFailure
116117
import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation
117118
import org.utbot.framework.plugin.api.UtTimeoutException
118119
import org.utbot.framework.plugin.api.UtVoidModel
@@ -149,6 +150,7 @@ import org.utbot.framework.util.isUnit
149150
import org.utbot.summary.SummarySentenceConstants.TAB
150151
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
151152
import java.lang.reflect.InvocationTargetException
153+
import java.security.AccessControlException
152154
import kotlin.reflect.jvm.javaType
153155

154156
private const val DEEP_EQUALS_MAX_DEPTH = 5 // TODO move it to plugin settings?
@@ -368,6 +370,11 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
368370
methodType = CRASH
369371
writeWarningAboutCrash()
370372
}
373+
is AccessControlException -> {
374+
methodType = CRASH
375+
writeWarningAboutFailureTest(exception)
376+
return
377+
}
371378
else -> {
372379
methodType = FAILING
373380
writeWarningAboutFailureTest(exception)
@@ -378,6 +385,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
378385
}
379386

380387
private fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean {
388+
if (exception is AccessControlException) return false
381389
// tests with timeout or crash should be processed differently
382390
if (exception is TimeoutException || exception is ConcreteExecutionFailureException) return false
383391

@@ -1618,6 +1626,12 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
16181626
)
16191627
}
16201628

1629+
if (result is UtSandboxFailure) {
1630+
testFrameworkManager.disableTestMethod(
1631+
"Disabled due to sandbox"
1632+
)
1633+
}
1634+
16211635
val testMethod = buildTestMethod {
16221636
name = methodName
16231637
parameters = params

utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.utbot.framework.plugin.api.UtInstrumentation
2020
import org.utbot.framework.plugin.api.UtMethod
2121
import org.utbot.framework.plugin.api.UtModel
2222
import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation
23+
import org.utbot.framework.plugin.api.UtSandboxFailure
2324
import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation
2425
import org.utbot.framework.plugin.api.UtTimeoutException
2526
import org.utbot.framework.plugin.api.util.UtContext
@@ -37,6 +38,7 @@ import org.utbot.instrumentation.instrumentation.et.ExplicitThrowInstruction
3738
import org.utbot.instrumentation.instrumentation.et.TraceHandler
3839
import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter
3940
import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor
41+
import java.security.AccessControlException
4042
import java.security.ProtectionDomain
4143
import java.util.IdentityHashMap
4244
import kotlin.reflect.jvm.javaMethod
@@ -212,6 +214,9 @@ object UtExecutionInstrumentation : Instrumentation<UtConcreteExecutionResult> {
212214
if (exception is TimeoutException) {
213215
return UtTimeoutException(exception)
214216
}
217+
if (exception is AccessControlException) {
218+
return UtSandboxFailure(exception)
219+
}
215220
val instrs = traceHandler.computeInstructionList()
216221
val isNested = if (instrs.isEmpty()) {
217222
false

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcess.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import java.io.File
1111
import java.io.OutputStream
1212
import java.io.PrintStream
1313
import java.net.URLClassLoader
14+
import java.security.AllPermission
1415
import java.time.LocalDateTime
1516
import java.time.format.DateTimeFormatter
1617
import kotlin.system.exitProcess
@@ -51,6 +52,12 @@ private val kryoHelper: KryoHelper = KryoHelper(System.`in`, System.`out`)
5152
* It should be compiled into separate jar file (child_process.jar) and be run with an agent (agent.jar) option.
5253
*/
5354
fun main() {
55+
permissions {
56+
// Enable all permissions for instrumentation.
57+
// SecurityKt.sandbox() is used to restrict these permissions.
58+
+ AllPermission()
59+
}
60+
5461
// We don't want user code to litter the standard output, so we redirect it.
5562
val tmpStream = PrintStream(object : OutputStream() {
5663
override fun write(b: Int) {}
@@ -127,7 +134,7 @@ private fun loop(instrumentation: Instrumentation<*>) {
127134
}
128135
System.err.println("warmup finished in $time ms")
129136
}
130-
is Protocol.InvokeMethodCommand -> {
137+
is Protocol.InvokeMethodCommand -> sandbox {
131138
val resultCmd = try {
132139
val clazz = HandlerClassesLoader.loadClass(cmd.className)
133140
val res = instrumentation.invoke(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package org.utbot.instrumentation.process
2+
3+
import sun.security.provider.PolicyFile
4+
import java.lang.reflect.ReflectPermission
5+
import java.net.URI
6+
import java.nio.file.Files
7+
import java.nio.file.Paths
8+
import java.security.AccessControlContext
9+
import java.security.AccessController
10+
import java.security.CodeSource
11+
import java.security.Permission
12+
import java.security.PermissionCollection
13+
import java.security.Permissions
14+
import java.security.Policy
15+
import java.security.PrivilegedAction
16+
import java.security.ProtectionDomain
17+
import java.security.cert.Certificate
18+
19+
internal fun permissions(block: SimplePolicy.() -> Unit) {
20+
val policy = Policy.getPolicy()
21+
if (policy !is SimplePolicy) {
22+
Policy.setPolicy(SimplePolicy(block))
23+
System.setSecurityManager(SecurityManager())
24+
} else {
25+
policy.block()
26+
}
27+
}
28+
29+
/**
30+
* Run [block] in sandbox mode.
31+
*
32+
* When running in sandbox by default only necessary to instrumentation permissions are enabled.
33+
* Other options are not enabled by default and rises [java.security.AccessControlException].
34+
*
35+
* To add new permissions create and/or edit file "{user.home}/.utbot/sandbox.policy".
36+
*
37+
* For example to enable property reading (`System.getProperty("user.home")`):
38+
*
39+
* ```
40+
* grant {
41+
* permission java.util.PropertyPermission "user.home", "read";
42+
* };
43+
* ```
44+
* Read more [about policy file and syntax](https://docs.oracle.com/javase/7/docs/technotes/guides/security/PolicyFiles.html#Examples)
45+
*/
46+
internal fun <T> sandbox(block: () -> T): T {
47+
val policyPath = Paths.get(System.getProperty("user.home"), ".utbot", "sandbox.policy")
48+
return sandbox(policyPath.toUri()) { block() }
49+
}
50+
51+
internal fun <T> sandbox(file: URI, block: () -> T): T {
52+
val path = Paths.get(file)
53+
val perms = mutableListOf<Permission>(
54+
RuntimePermission("accessDeclaredMembers"),
55+
RuntimePermission("getProtectionDomain"),
56+
RuntimePermission("accessClassInPackage.*"),
57+
ReflectPermission("*"),
58+
)
59+
if (Files.exists(path)) {
60+
val policyFile = PolicyFile(file.toURL())
61+
val collection = policyFile.getPermissions(CodeSource(null, emptyArray<Certificate>()))
62+
perms += collection.elements().toList()
63+
}
64+
return sandbox(perms) { block() }
65+
}
66+
67+
internal fun <T> sandbox(permission: List<Permission>, block: () -> T): T {
68+
val perms = permission.fold(Permissions()) { acc, p -> acc.add(p); acc }
69+
return sandbox(perms) { block() }
70+
}
71+
72+
internal fun <T> sandbox(perms: PermissionCollection, block: () -> T): T {
73+
val acc = AccessControlContext(arrayOf(ProtectionDomain(null, perms)))
74+
return AccessController.doPrivileged(
75+
PrivilegedAction {
76+
block()
77+
},
78+
acc
79+
)
80+
}
81+
82+
/**
83+
* This policy can add grant or denial rules for permissions.
84+
*
85+
* To add a grant permission use like this in any place:
86+
*
87+
* ```
88+
* permissions {
89+
* + java.security.PropertyPolicy("user.home", "read,write")
90+
* }
91+
* ```
92+
*
93+
* After first call [SecurityManager] is set with this policy
94+
*
95+
* To deny a permission:
96+
*
97+
* ```
98+
* permissions {
99+
* - java.security.PropertyPolicy("user.home", "read,write")
100+
* }
101+
* ```
102+
*
103+
* To delete all concrete permissions (if it was added before):
104+
*
105+
* ```
106+
* permissions {
107+
* ! java.security.PropertyPolicy("user.home", "read,write")
108+
* }
109+
* ```
110+
*
111+
* The last permission has priority. Enable all property read for "user.*", but forbid to read only "user.home":
112+
*
113+
* ```
114+
* permissions {
115+
* + java.security.PropertyPolicy("user.*", "read,write")
116+
* - java.security.PropertyPolicy("user.home", "read,write")
117+
* }
118+
* ```
119+
*/
120+
internal class SimplePolicy(init: SimplePolicy.() -> Unit = {}) : Policy() {
121+
sealed class Access(val permission: Permission) {
122+
class Allow(permission: Permission) : Access(permission)
123+
class Deny(permission: Permission) : Access(permission)
124+
}
125+
private var permissions = mutableListOf<Access>()
126+
127+
init { apply(init) }
128+
129+
operator fun Permission.unaryPlus() = permissions.add(Access.Allow(this))
130+
131+
operator fun Permission.unaryMinus() = permissions.add(Access.Deny(this))
132+
133+
operator fun Permission.not() = permissions.removeAll { it.permission == this }
134+
135+
override fun getPermissions(codesource: CodeSource) = UNSUPPORTED_EMPTY_COLLECTION!!
136+
override fun getPermissions(domain: ProtectionDomain) = UNSUPPORTED_EMPTY_COLLECTION!!
137+
override fun implies(domain: ProtectionDomain, permission: Permission): Boolean {
138+
// 0 means no info, < 0 is denied and > 0 is allowed
139+
val result = permissions.lastOrNull { it.permission.implies(permission) }?.let {
140+
when (it) {
141+
is Access.Allow -> 1
142+
is Access.Deny -> -1
143+
}
144+
} ?: 0
145+
return result > 0
146+
}
147+
}

utbot-summary/src/main/kotlin/org/utbot/summary/TagGenerator.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.utbot.framework.plugin.api.UtExplicitlyThrownException
99
import org.utbot.framework.plugin.api.UtImplicitlyThrownException
1010
import org.utbot.framework.plugin.api.UtOverflowFailure
1111
import org.utbot.framework.plugin.api.UtMethodTestSet
12+
import org.utbot.framework.plugin.api.UtSandboxFailure
1213
import org.utbot.framework.plugin.api.UtTimeoutException
1314
import org.utbot.framework.plugin.api.util.isCheckedException
1415
import org.utbot.summary.UtSummarySettings.MIN_NUMBER_OF_EXECUTIONS_FOR_CLUSTERING
@@ -140,7 +141,8 @@ enum class ClusterKind {
140141
EXPLICITLY_THROWN_UNCHECKED_EXCEPTIONS,
141142
OVERFLOWS,
142143
TIMEOUTS,
143-
CRASH_SUITE;
144+
CRASH_SUITE,
145+
SECURITY;
144146

145147
val displayName: String get() = toString().replace('_', ' ')
146148
}
@@ -152,6 +154,7 @@ private fun UtExecutionResult.clusterKind() = when (this) {
152154
is UtOverflowFailure -> ClusterKind.OVERFLOWS
153155
is UtTimeoutException -> ClusterKind.TIMEOUTS
154156
is UtConcreteExecutionFailure -> ClusterKind.CRASH_SUITE
157+
is UtSandboxFailure -> ClusterKind.SECURITY
155158
}
156159

157160
/**

0 commit comments

Comments
 (0)