Skip to content

Commit 57b06d3

Browse files
committed
Run evaluation of inlined quotes in secured sandbox
1 parent 3821b16 commit 57b06d3

File tree

10 files changed

+173
-3
lines changed

10 files changed

+173
-3
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package transform
33

44
import java.io.{PrintWriter, StringWriter}
55
import java.lang.reflect.Method
6-
import java.net.URLClassLoader
76

87
import dotty.tools.dotc.ast.tpd
98
import dotty.tools.dotc.core.Contexts._
@@ -14,6 +13,7 @@ import dotty.tools.dotc.core.Names.Name
1413
import dotty.tools.dotc.core.quoted._
1514
import dotty.tools.dotc.core.Types._
1615
import dotty.tools.dotc.core.Symbols._
16+
import dotty.tools.dotc.util.Sandbox
1717

1818
import scala.util.control.NonFatal
1919
import dotty.tools.dotc.util.Positions.Position
@@ -73,8 +73,10 @@ object Splicer {
7373
}
7474

7575
private def evaluateLambda(lambda: Seq[Any] => Object, args: Seq[Any], pos: Position)(implicit ctx: Context): Option[scala.quoted.Expr[Nothing]] = {
76-
try Some(lambda(args).asInstanceOf[scala.quoted.Expr[Nothing]])
77-
catch {
76+
try {
77+
val res = Sandbox.runInSecuredThread(lambda(args))
78+
Some(res.asInstanceOf[scala.quoted.Expr[Nothing]])
79+
} catch {
7880
case ex: scala.quoted.QuoteError =>
7981
ctx.error(ex.getMessage, pos)
8082
None
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 to 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)