Skip to content

Commit e149118

Browse files
committed
Run evaluation of inlined quotes in secured sandbox
1 parent 345a808 commit e149118

File tree

10 files changed

+146
-1
lines changed

10 files changed

+146
-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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package dotty.tools.dotc.util
2+
3+
import java.io.FilePermission
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+
// Implementation assumes single threaded
16+
val oldSecurityManager = System.getSecurityManager
17+
try {
18+
val securityManager = new SandboxSecurityManager
19+
System.setSecurityManager(securityManager)
20+
class SandboxThread extends Thread {
21+
var result: scala.util.Try[T] =
22+
scala.util.Failure(new Exception("Sandbox failed with a fatal error"))
23+
override def run(): Unit = {
24+
result = scala.util.Try {
25+
securityManager.enable() // Enable security manager on this thread
26+
thunk
27+
}
28+
}
29+
}
30+
val thread = new SandboxThread
31+
thread.start()
32+
thread.join(timeout)
33+
if (thread.isAlive) {
34+
// TODO kill the thread?
35+
throw new InvocationTargetException(new QuoteError(s"Failed to evaluate inlined quote. Caused by timeout ($timeout ms)."))
36+
} else thread.result.fold[T](throw _, identity)
37+
} finally {
38+
System.setSecurityManager(oldSecurityManager)
39+
}
40+
}
41+
42+
private class SandboxSecurityManager extends SecurityManager {
43+
private[this] val enabledFlag: ThreadLocal[Boolean] = new ThreadLocal[Boolean]() {
44+
override protected def initialValue(): Boolean = false
45+
}
46+
47+
def enable(): Unit = {
48+
enabledFlag.set(true)
49+
}
50+
51+
override def checkPermission(permission: Permission): Unit = {
52+
if (enabledFlag.get()) {
53+
permission match {
54+
case permission: FilePermission if permission.getActions == "read" && isClassLoading =>
55+
// allow file reads in the class loader
56+
case _: RuntimePermission if isClassLoading =>
57+
// allow runtime permission in the class loader
58+
case _: ReflectPermission =>
59+
// allow reflection, needed for interpreter
60+
// TODO restrict more?
61+
case _ => super.checkPermission(permission)
62+
}
63+
}
64+
}
65+
66+
override def checkPermission(permission: Permission, context: Object): Unit = {
67+
if (enabledFlag.get())
68+
super.checkPermission(permission, context)
69+
}
70+
71+
private def isClassLoading: Boolean = {
72+
try {
73+
enabledFlag.set(false) // Disable security do the check
74+
Thread.currentThread().getStackTrace.exists(elem => elem.getClassName == "java.lang.ClassLoader")
75+
} finally {
76+
enabledFlag.set(true)
77+
}
78+
}
79+
}
80+
}
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)