Skip to content

Commit 6fc1527

Browse files
committed
Run evaluation of inlined quotes in secured sandbox
1 parent 7221287 commit 6fc1527

File tree

10 files changed

+170
-1
lines changed

10 files changed

+170
-1
lines changed

compiler/src/dotty/tools/dotc/transform/Splicer.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import dotty.tools.dotc.core.Contexts._
66
import dotty.tools.dotc.core.Decorators._
77
import dotty.tools.dotc.core.quoted._
88
import dotty.tools.dotc.interpreter._
9+
import dotty.tools.dotc.util.Sandbox
910

1011
import scala.util.control.NonFatal
1112

@@ -28,7 +29,7 @@ object Splicer {
2829
private def reflectiveSplice(tree: Tree)(implicit ctx: Context): Tree = {
2930
val interpreter = new Interpreter
3031
val interpreted =
31-
try interpreter.interpretTree[scala.quoted.Expr[_]](tree)
32+
try Sandbox.runInSecuredThread(interpreter.interpretTree[scala.quoted.Expr[_]](tree))
3233
catch { case ex: InvocationTargetException => handleTargetException(tree, ex); None }
3334
interpreted.fold(tree)(PickledQuotes.quotedExprToTree)
3435
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package dotty.tools.dotc.util
2+
3+
4+
import java.lang.reflect.{InvocationTargetException, ReflectPermission}
5+
import java.security.Permission
6+
7+
import scala.quoted.QuoteError
8+
9+
object Sandbox {
10+
11+
/** Timeout in milliseconds */
12+
final val timeout = 3000 // TODO add a flag to allow custom timeouts
13+
14+
def runInSecuredThread[T](thunk: => T): T = {
15+
runWithSandboxSecurityManager { securityManager =>
16+
class SandboxThread extends Thread {
17+
var result: scala.util.Try[T] =
18+
scala.util.Failure(new Exception("Sandbox failed with a fatal error"))
19+
override def run(): Unit = {
20+
result = scala.util.Try {
21+
securityManager.enable() // Enable security manager on this thread
22+
thunk
23+
}
24+
}
25+
}
26+
val thread = new SandboxThread
27+
thread.start()
28+
thread.join(timeout)
29+
if (thread.isAlive) {
30+
// TODO kill the thread?
31+
throw new InvocationTargetException(new QuoteError(s"Failed to evaluate inlined quote. Caused by timeout ($timeout ms)."))
32+
} else thread.result.fold[T](throw _, identity)
33+
}
34+
}
35+
36+
private def runWithSandboxSecurityManager[T](run: SandboxSecurityManager => T): T = {
37+
val ssm: SandboxSecurityManager = synchronized {
38+
System.getSecurityManager match {
39+
case ssm: SandboxSecurityManager =>
40+
assert(ssm.running > 0)
41+
ssm.running += 1
42+
ssm
43+
case sm =>
44+
assert(sm == null)
45+
val ssm = new SandboxSecurityManager
46+
System.setSecurityManager(ssm)
47+
ssm
48+
}
49+
}
50+
try run(ssm)
51+
finally synchronized {
52+
ssm.running -= 1
53+
assert(ssm.running >= 0)
54+
if (ssm.running == 0) {
55+
assert(System.getSecurityManager eq ssm)
56+
System.setSecurityManager(null)
57+
}
58+
}
59+
}
60+
61+
/** A security manager that can be enabled on individual threads.
62+
*
63+
* Inspired by https://github.com/alphaloop/selective-security-manager
64+
*/
65+
private class SandboxSecurityManager extends SecurityManager {
66+
67+
@volatile private[Sandbox] var running: Int = 1
68+
69+
private[this] val enabledFlag: ThreadLocal[Boolean] = new ThreadLocal[Boolean]() {
70+
override protected def initialValue(): Boolean = false
71+
}
72+
73+
def enable(): Unit = {
74+
enabledFlag.set(true)
75+
}
76+
77+
override def checkPermission(permission: Permission): Unit = {
78+
if (enabledFlag.get()) {
79+
permission match {
80+
case _: ReflectPermission =>
81+
// allow reflection, needed for interpreter
82+
// TODO restrict more?
83+
case _ if isClassLoading =>
84+
// allow any permission in the class loader
85+
case _ => super.checkPermission(permission)
86+
}
87+
}
88+
}
89+
90+
override def checkPermission(permission: Permission, context: Object): Unit = {
91+
if (enabledFlag.get())
92+
super.checkPermission(permission, context)
93+
}
94+
95+
private def isClassLoading: Boolean = {
96+
try {
97+
enabledFlag.set(false) // Disable security do the check
98+
Thread.currentThread().getStackTrace.exists(elem => elem.getClassName == "java.lang.ClassLoader")
99+
} finally {
100+
enabledFlag.set(true)
101+
}
102+
}
103+
}
104+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import java.io.File
2+
3+
import scala.quoted._
4+
object Macros {
5+
inline def foo(): Int = ~fooImpl()
6+
def fooImpl(): Expr[Int] = {
7+
System.setSecurityManager(null)
8+
'(1)
9+
}
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Macros._
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
Macros.foo() // error: Failed to evaluate inlined quote. Caused by: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "setSecurityManager")
5+
}
6+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import java.io.File
2+
3+
import scala.quoted._
4+
object Macros {
5+
inline def foo(): Int = ~fooImpl()
6+
def fooImpl(): Expr[Int] = {
7+
(new File("dfsdafsd")).exists()
8+
'(1)
9+
}
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Macros._
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
Macros.foo() // error: Failed to evaluate inlined quote. Caused by: access denied ("java.util.PropertyPermission" "user.dir" "read")
5+
}
6+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import java.io.File
2+
3+
import scala.quoted._
4+
object Macros {
5+
inline def foo(): Int = ~fooImpl()
6+
def fooImpl(): Expr[Int] = {
7+
(new File("dfsdafsd")).createNewFile()
8+
'(1)
9+
}
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Macros._
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
Macros.foo() // error: Failed to evaluate inlined quote. Caused by: access denied ("java.util.PropertyPermission" "user.dir" "read")
5+
}
6+
}

tests/neg/quote-timeout/Macro_1.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import java.io.File
2+
3+
import scala.quoted._
4+
object Macros {
5+
inline def foo(): Int = ~fooImpl()
6+
def fooImpl(): Expr[Int] = {
7+
while (true) ()
8+
'(1)
9+
}
10+
}

tests/neg/quote-timeout/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Macros._
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
Macros.foo() // error: Failed to evaluate inlined quote. Caused by timeout (3000 ms).
5+
}
6+
}

0 commit comments

Comments
 (0)