diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 8b39883d51c0..13bf4d829ee9 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -46,6 +46,9 @@ class ScalaSettings extends Settings.SettingGroup { val silentWarnings = BooleanSetting("-nowarn", "Silence all warnings.") val fromTasty = BooleanSetting("-from-tasty", "Compile classes from tasty in classpath. The arguments are used as class names.") + /** Macro settings */ + val macroTimeout = IntSetting("-macro-timeout", "Timeout for the evaluation of a macro in ms", 1000, 1 to Int.MaxValue) + /** Decompiler settings */ val printTasty = BooleanSetting("-print-tasty", "Prints the raw tasty when decompiling.") val printLines = BooleanSetting("-print-lines", "Show source code line numbers.") diff --git a/compiler/src/dotty/tools/dotc/transform/Splicer.scala b/compiler/src/dotty/tools/dotc/transform/Splicer.scala index 3877c36cdb59..0ad51bf3f954 100644 --- a/compiler/src/dotty/tools/dotc/transform/Splicer.scala +++ b/compiler/src/dotty/tools/dotc/transform/Splicer.scala @@ -3,7 +3,6 @@ package transform import java.io.{PrintWriter, StringWriter} import java.lang.reflect.Method -import java.net.URLClassLoader import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.core.Contexts._ @@ -14,6 +13,7 @@ import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.quoted._ import dotty.tools.dotc.core.Types._ import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.util.Sandbox import scala.util.control.NonFatal import dotty.tools.dotc.util.Positions.Position @@ -73,8 +73,11 @@ object Splicer { } private def evaluateLambda(lambda: Seq[Any] => Object, args: Seq[Any], pos: Position)(implicit ctx: Context): Option[scala.quoted.Expr[Nothing]] = { - try Some(lambda(args).asInstanceOf[scala.quoted.Expr[Nothing]]) - catch { + try { + val timeout = ctx.settings.macroTimeout.value + val res = Sandbox.runInSecuredThread(timeout)(lambda(args)) + Some(res.asInstanceOf[scala.quoted.Expr[Nothing]]) + } catch { case ex: scala.quoted.QuoteError => ctx.error(ex.getMessage, pos) None diff --git a/compiler/src/dotty/tools/dotc/util/Sandbox.scala b/compiler/src/dotty/tools/dotc/util/Sandbox.scala new file mode 100644 index 000000000000..2504975fd4e9 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/util/Sandbox.scala @@ -0,0 +1,93 @@ +package dotty.tools.dotc.util + + +import java.lang.reflect.{InvocationTargetException, ReflectPermission} +import java.security.Permission + +import scala.quoted.QuoteError + +object Sandbox { + + def runInSecuredThread[T](timeout: Int)(thunk: => T): T = { + runWithSandboxSecurityManager { securityManager => + class SandboxThread extends Thread { + var result: scala.util.Try[T] = + scala.util.Failure(new Exception("Sandbox failed with a fatal error")) + override def run(): Unit = { + result = scala.util.Try { + securityManager.enable() // Enable security manager on this thread + thunk + } + } + } + val thread = new SandboxThread + thread.start() + thread.join(timeout) + if (thread.isAlive) { + // TODO kill the thread? + throw new InvocationTargetException(new QuoteError(s"Failed to evaluate inlined quote. Caused by timeout ($timeout ms).")) + } else thread.result.fold[T](throw _, identity) + } + } + + private def runWithSandboxSecurityManager[T](run: SandboxSecurityManager => T): T = { + val ssm: SandboxSecurityManager = synchronized { + System.getSecurityManager match { + case ssm: SandboxSecurityManager => + assert(ssm.running > 0) + ssm.running += 1 + ssm + case sm => + assert(sm == null) + val ssm = new SandboxSecurityManager + System.setSecurityManager(ssm) + ssm + } + } + try run(ssm) + finally synchronized { + ssm.running -= 1 + assert(ssm.running >= 0) + if (ssm.running == 0) { + assert(System.getSecurityManager eq ssm) + System.setSecurityManager(null) + } + } + } + + /** A security manager that can be enabled on individual threads. + * + * Inspired by https://github.com/alphaloop/selective-security-manager + */ + private class SandboxSecurityManager extends SecurityManager { + + @volatile private[Sandbox] var running: Int = 1 + + private[this] val enabledFlag: ThreadLocal[Boolean] = new ThreadLocal[Boolean]() { + override protected def initialValue(): Boolean = false + } + + def enable(): Unit = { + enabledFlag.set(true) + } + + override def checkPermission(permission: Permission): Unit = { + if (enabledFlag.get() && !isClassLoading) + super.checkPermission(permission) + } + + override def checkPermission(permission: Permission, context: Object): Unit = { + if (enabledFlag.get()) + super.checkPermission(permission, context) + } + + private def isClassLoading: Boolean = { + try { + enabledFlag.set(false) // Disable security to do the check + Thread.currentThread().getStackTrace.exists(elem => elem.getClassName == "java.lang.ClassLoader") + } finally { + enabledFlag.set(true) + } + } + } +} diff --git a/tests/neg/quote-security-1/Macro_1.scala b/tests/neg/quote-security-1/Macro_1.scala new file mode 100644 index 000000000000..ed0c413dd1f4 --- /dev/null +++ b/tests/neg/quote-security-1/Macro_1.scala @@ -0,0 +1,10 @@ +import java.io.File + +import scala.quoted._ +object Macros { + inline def foo(): Int = ~fooImpl() + def fooImpl(): Expr[Int] = { + System.setSecurityManager(null) + '(1) + } +} diff --git a/tests/neg/quote-security-1/Test_2.scala b/tests/neg/quote-security-1/Test_2.scala new file mode 100644 index 000000000000..9b6ac07c5550 --- /dev/null +++ b/tests/neg/quote-security-1/Test_2.scala @@ -0,0 +1,6 @@ +import Macros._ +object Test { + def main(args: Array[String]): Unit = { + Macros.foo() // error: Failed to evaluate inlined quote. Caused by: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "setSecurityManager") + } +} diff --git a/tests/neg/quote-security-2/Macro_1.scala b/tests/neg/quote-security-2/Macro_1.scala new file mode 100644 index 000000000000..4e9d8fac2f76 --- /dev/null +++ b/tests/neg/quote-security-2/Macro_1.scala @@ -0,0 +1,10 @@ +import java.io.File + +import scala.quoted._ +object Macros { + inline def foo(): Int = ~fooImpl() + def fooImpl(): Expr[Int] = { + (new File("dfsdafsd")).exists() + '(1) + } +} diff --git a/tests/neg/quote-security-2/Test_2.scala b/tests/neg/quote-security-2/Test_2.scala new file mode 100644 index 000000000000..48ec2bd9c5d4 --- /dev/null +++ b/tests/neg/quote-security-2/Test_2.scala @@ -0,0 +1,6 @@ +import Macros._ +object Test { + def main(args: Array[String]): Unit = { + Macros.foo() // error: Failed to evaluate inlined quote. Caused by: access denied ("java.util.PropertyPermission" "user.dir" "read") + } +} diff --git a/tests/neg/quote-security-3/Macro_1.scala b/tests/neg/quote-security-3/Macro_1.scala new file mode 100644 index 000000000000..829c5327b4ba --- /dev/null +++ b/tests/neg/quote-security-3/Macro_1.scala @@ -0,0 +1,10 @@ +import java.io.File + +import scala.quoted._ +object Macros { + inline def foo(): Int = ~fooImpl() + def fooImpl(): Expr[Int] = { + (new File("dfsdafsd")).createNewFile() + '(1) + } +} diff --git a/tests/neg/quote-security-3/Test_2.scala b/tests/neg/quote-security-3/Test_2.scala new file mode 100644 index 000000000000..48ec2bd9c5d4 --- /dev/null +++ b/tests/neg/quote-security-3/Test_2.scala @@ -0,0 +1,6 @@ +import Macros._ +object Test { + def main(args: Array[String]): Unit = { + Macros.foo() // error: Failed to evaluate inlined quote. Caused by: access denied ("java.util.PropertyPermission" "user.dir" "read") + } +} diff --git a/tests/neg/quote-timeout/Macro_1.scala b/tests/neg/quote-timeout/Macro_1.scala new file mode 100644 index 000000000000..e311d8a92e2a --- /dev/null +++ b/tests/neg/quote-timeout/Macro_1.scala @@ -0,0 +1,10 @@ +import java.io.File + +import scala.quoted._ +object Macros { + inline def foo(): Int = ~fooImpl() + def fooImpl(): Expr[Int] = { + while (true) () + '(1) + } +} diff --git a/tests/neg/quote-timeout/Test_2.scala b/tests/neg/quote-timeout/Test_2.scala new file mode 100644 index 000000000000..cf7b1788e6ae --- /dev/null +++ b/tests/neg/quote-timeout/Test_2.scala @@ -0,0 +1,6 @@ +import Macros._ +object Test { + def main(args: Array[String]): Unit = { + Macros.foo() // error: Failed to evaluate inlined quote. Caused by timeout (3000 ms). + } +}