scala/CVE-2017-15288.patch

507 lines
21 KiB
Diff
Raw Permalink Normal View History

2019-12-26 16:38:01 +08:00
From 67e1437e55df6789d0883cb8846d12071de75c63 Mon Sep 17 00:00:00 2001
From: Jason Zaugg <jzaugg@gmail.com>
Date: Mon, 2 Oct 2017 10:06:55 +1000
Subject: [PATCH] Move compilation daemon portfile under `~/.scalac/`
Store the compilation daemon's administrativia (port file, redirection)
under `~/.scalac/`, instead of the less standard
`/tmp/scala-devel/${USER:shared}/scalac-compile-server-port`.
On creation, remove group- and other-permissions from these
private files, ditto for the repl's history file.
On Java 6 on Windows, opt in to compilation daemon using `-nc:false`.
Cherry picked from b64ad85, aa133c9, 2ceb09c
---
.../scala/tools/nsc/CompileServer.scala | 22 ++--
.../scala/tools/nsc/CompileSocket.scala | 68 ++++++-----
.../tools/nsc/GenericRunnerSettings.scala | 5 +-
src/compiler/scala/tools/nsc/Properties.scala | 5 +
.../scala/tools/nsc/ScriptRunner.scala | 20 +++-
.../session/FileBackedHistory.scala | 32 +++++-
.../tools/nsc/util/ScalaClassLoader.scala | 27 ++---
.../internal/util/OwnerOnlyChmod.scala | 107 ++++++++++++++++++
8 files changed, 221 insertions(+), 65 deletions(-)
create mode 100644 src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala
diff --git a/src/compiler/scala/tools/nsc/CompileServer.scala b/src/compiler/scala/tools/nsc/CompileServer.scala
index 6352d75686a..c454ba8b62b 100644
--- a/src/compiler/scala/tools/nsc/CompileServer.scala
+++ b/src/compiler/scala/tools/nsc/CompileServer.scala
@@ -183,14 +183,15 @@ object CompileServer {
execute(() => (), args)
/**
- * Used for internal testing. The callback is called upon
- * server start, notifying the caller that the server is
- * ready to run. WARNING: the callback runs in the
- * server's thread, blocking the server from doing any work
- * until the callback is finished. Callbacks should be kept
- * simple and clients should not try to interact with the
- * server while the callback is processing.
- */
+ * The server's main loop.
+ *
+ * `startupCallback` is used for internal testing; it's called upon server start,
+ * notifying the caller that the server is ready to run.
+ *
+ * WARNING: the callback runs in the server's thread, blocking the server from doing any work
+ * until the callback is finished. Callbacks should be kept simple and clients should not try to
+ * interact with the server while the callback is processing.
+ */
def execute(startupCallback : () => Unit, args: Array[String]) {
val debug = args contains "-v"
var port = 0
@@ -198,14 +199,13 @@ object CompileServer {
val i = args.indexOf("-p")
if (i >= 0 && args.length > i + 1) {
scala.util.control.Exception.ignoring(classOf[NumberFormatException]) {
- port = args(i + 1).toInt
+ port = args(i + 1).toInt
}
}
// Create instance rather than extend to pass a port parameter.
val server = new StandardCompileServer(port)
- val redirectDir = (server.compileSocket.tmpDir / "output-redirects").createDirectory()
-
+ val redirectDir = server.compileSocket.mkDaemonDir("fsc_redirects")
if (debug) {
server.echo("Starting CompileServer on port " + server.port)
server.echo("Redirect dir is " + redirectDir)
diff --git a/src/compiler/scala/tools/nsc/CompileSocket.scala b/src/compiler/scala/tools/nsc/CompileSocket.scala
index f5039b8303f..b73d251e9cc 100644
--- a/src/compiler/scala/tools/nsc/CompileSocket.scala
+++ b/src/compiler/scala/tools/nsc/CompileSocket.scala
@@ -5,13 +5,17 @@
package scala.tools.nsc
-import java.io.FileNotFoundException
+import java.math.BigInteger
import java.security.SecureRandom
+import scala.io.Codec
+import scala.reflect.internal.util.OwnerOnlyChmod
import scala.reflect.internal.util.StringOps.splitWhere
import scala.sys.process._
-import scala.tools.nsc.io.{File, Path, Socket}
+import scala.tools.nsc.Properties.scalacDir
+import scala.tools.nsc.io.{File, Socket}
import scala.tools.util.CompileOutputCommon
+import scala.util.control.NonFatal
trait HasCompileSocket {
def compileSocket: CompileSocket
@@ -46,14 +50,10 @@ trait HasCompileSocket {
class CompileSocket extends CompileOutputCommon {
protected lazy val compileClient: StandardCompileClient = CompileClient
def verbose = compileClient.verbose
-
+ def verbose_=(v: Boolean) = compileClient.verbose = v
/* Fixes the port where to start the server, 0 yields some free port */
var fixPort = 0
- /** The prefix of the port identification file, which is followed
- * by the port number.
- */
- protected lazy val dirName = "scalac-compile-server-port"
protected def cmdName = Properties.scalaCmd
/** The vm part of the command to start a new scala compile server */
@@ -69,20 +69,8 @@ class CompileSocket extends CompileOutputCommon {
protected val serverClass = "scala.tools.nsc.CompileServer"
protected def serverClassArgs = (if (verbose) List("-v") else Nil) ::: (if (fixPort > 0) List("-p", fixPort.toString) else Nil)
- /** A temporary directory to use */
- val tmpDir = {
- val udir = Option(Properties.userName) getOrElse "shared"
- val f = (Path(Properties.tmpDir) / ("scala-devel" + udir)).createDirectory()
-
- if (f.isDirectory && f.canWrite) {
- info("[Temp directory: " + f + "]")
- f
- }
- else fatal("Could not find a directory for temporary files")
- }
-
/* A directory holding port identification files */
- val portsDir = (tmpDir / dirName).createDirectory()
+ private lazy val portsDir = mkDaemonDir("fsc_port")
/** The command which starts the compile server, given vm arguments.
*
@@ -104,7 +92,7 @@ class CompileSocket extends CompileOutputCommon {
}
/** The port identification file */
- def portFile(port: Int) = portsDir / File(port.toString)
+ def portFile(port: Int): File = portsDir / File(port.toString)
/** Poll for a server port number; return -1 if none exists yet */
private def pollPort(): Int = if (fixPort > 0) {
@@ -138,19 +126,19 @@ class CompileSocket extends CompileOutputCommon {
}
info("[Port number: " + port + "]")
if (port < 0)
- fatal("Could not connect to compilation daemon after " + attempts + " attempts.")
+ fatal(s"Could not connect to compilation daemon after $attempts attempts. To run without it, use `-nocompdaemon` or `-nc`.")
port
}
/** Set the port number to which a scala compile server is connected */
- def setPort(port: Int) {
- val file = portFile(port)
- val secret = new SecureRandom().nextInt.toString
-
- try file writeAll secret catch {
- case e @ (_: FileNotFoundException | _: SecurityException) =>
- fatal("Cannot create file: %s".format(file.path))
- }
+ def setPort(port: Int): Unit = {
+ val file = portFile(port)
+ // 128 bits of delicious randomness, suitable for printing with println over a socket,
+ // and storage in a file -- see getPassword
+ val secretDigits = new BigInteger(128, new SecureRandom()).toString.getBytes("UTF-8")
+
+ try OwnerOnlyChmod().chmodAndWrite(file.jfile, secretDigits)
+ catch chmodFailHandler(s"Cannot create file: ${file}")
}
/** Delete the port number to which a scala compile server was connected */
@@ -208,7 +196,7 @@ class CompileSocket extends CompileOutputCommon {
def getPassword(port: Int): String = {
val ff = portFile(port)
- val f = ff.bufferedReader()
+ val f = ff.bufferedReader(Codec.UTF8)
// allow some time for the server to start up
def check = {
@@ -223,6 +211,24 @@ class CompileSocket extends CompileOutputCommon {
f.close()
result
}
+
+ private def chmodFailHandler(msg: String): PartialFunction[Throwable, Unit] = {
+ case NonFatal(e) =>
+ if (verbose) e.printStackTrace()
+ fatal(msg)
+ }
+
+ def mkDaemonDir(name: String) = {
+ val dir = (scalacDir / name).createDirectory()
+
+ if (dir.isDirectory && dir.canWrite) info(s"[Temp directory: $dir]")
+ else fatal(s"Could not create compilation daemon directory $dir")
+
+ try OwnerOnlyChmod().chmod(dir.jfile)
+ catch chmodFailHandler(s"Failed to change permissions on $dir. The compilation daemon requires a secure directory; use -nc to disable the daemon.")
+ dir
+ }
+
}
diff --git a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala
index 9c2db11a56e..edfc095c7f7 100644
--- a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala
+++ b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala
@@ -38,8 +38,11 @@ class GenericRunnerSettings(error: String => Unit) extends Settings(error) {
val nc = BooleanSetting(
"-nc",
- "do not use the fsc compilation daemon") withAbbreviation "-nocompdaemon"
+ "do not use the fsc compilation daemon") withAbbreviation "-nocompdaemon" withPostSetHook((x: BooleanSetting) => {_useCompDaemon = !x.value })
@deprecated("Use `nc` instead", "2.9.0") def nocompdaemon = nc
@deprecated("Use `save` instead", "2.9.0") def savecompiled = save
+
+ private[this] var _useCompDaemon = true
+ def useCompDaemon: Boolean = _useCompDaemon
}
diff --git a/src/compiler/scala/tools/nsc/Properties.scala b/src/compiler/scala/tools/nsc/Properties.scala
index 55fd1967164..8b314ba0b82 100644
--- a/src/compiler/scala/tools/nsc/Properties.scala
+++ b/src/compiler/scala/tools/nsc/Properties.scala
@@ -5,6 +5,8 @@
package scala.tools.nsc
+import scala.tools.nsc.io.Path
+
/** Loads `compiler.properties` from the jar archive file.
*/
object Properties extends scala.util.PropertiesTrait {
@@ -22,4 +24,7 @@ object Properties extends scala.util.PropertiesTrait {
// derived values
def isEmacsShell = propOrEmpty("env.emacs") != ""
def fileEndings = fileEndingString.split("""\|""").toList
+
+ // Where we keep fsc's state (ports/redirection)
+ lazy val scalacDir = (Path(Properties.userHome) / ".scalac").createDirectory(force = false)
}
diff --git a/src/compiler/scala/tools/nsc/ScriptRunner.scala b/src/compiler/scala/tools/nsc/ScriptRunner.scala
index 107c4b3df3d..9af0079ffd6 100644
--- a/src/compiler/scala/tools/nsc/ScriptRunner.scala
+++ b/src/compiler/scala/tools/nsc/ScriptRunner.scala
@@ -77,7 +77,10 @@ class ScriptRunner extends HasCompileSocket {
val coreCompArgs = compSettings flatMap (_.unparse)
val compArgs = coreCompArgs ++ List("-Xscript", scriptMain(settings), scriptFile)
- CompileSocket getOrCreateSocket "" match {
+ // TODO: untangle this mess of top-level objects with their own little view of the mutable world of settings
+ compileSocket.verbose = settings.verbose.value
+
+ compileSocket getOrCreateSocket "" match {
case Some(sock) => compileOnServer(sock, compArgs)
case _ => false
}
@@ -109,14 +112,23 @@ class ScriptRunner extends HasCompileSocket {
settings.outdir.value = compiledPath.path
- if (settings.nc.value) {
- /** Setting settings.script.value informs the compiler this is not a
- * self contained compilation unit.
+ // can't reliably lock down permissions on the portfile in this environment => disable by default.
+ // not the cleanest to do this here, but I don't see where else to decide this and emit the warning below
+ val cantLockdown = !settings.nc.isSetByUser && scala.util.Properties.isWin && !scala.util.Properties.isJavaAtLeast("7")
+
+ if (cantLockdown) settings.nc.value = true
+
+ if (!settings.useCompDaemon) {
+ /* Setting settings.script.value informs the compiler this is not a
+ * self contained compilation unit.
*/
settings.script.value = mainClass
val reporter = new ConsoleReporter(settings)
val compiler = newGlobal(settings, reporter)
+ if (cantLockdown)
+ reporter.echo("[info] The compilation daemon is disabled by default on this platform. To force its usage, use `-nocompdaemon:false`.")
+
new compiler.Run compile List(scriptFile)
if (reporter.hasErrors) None else Some(compiledPath)
}
diff --git a/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala b/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala
index dddfb1b8f64..5467c0a61ef 100644
--- a/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala
+++ b/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala
@@ -7,14 +7,37 @@ package scala.tools.nsc
package interpreter
package session
-import scala.tools.nsc.io._
-import FileBackedHistory._
+import scala.reflect.internal.util.OwnerOnlyChmod
+import scala.reflect.io.{File, Path}
+import scala.tools.nsc.Properties.{propOrNone, userHome}
+import scala.util.control.NonFatal
/** TODO: file locking.
*/
trait FileBackedHistory extends JLineHistory with JPersistentHistory {
def maxSize: Int = 2500
- protected lazy val historyFile: File = defaultFile
+
+ // For a history file in the standard location, always try to restrict permission,
+ // creating an empty file if none exists.
+ // For a user-specified location, only lock down permissions if we're the ones
+ // creating it, otherwise responsibility for permissions is up to the caller.
+ protected lazy val historyFile: File = File {
+ propOrNone("scala.shell.histfile").map(Path.apply) match {
+ case Some(p) => if (!p.exists) secure(p) else p
+ case None => secure(Path(userHome) / FileBackedHistory.defaultFileName)
+ }
+ }
+
+ private def secure(p: Path): Path = {
+ try OwnerOnlyChmod().chmodOrCreateEmpty(p.jfile)
+ catch { case NonFatal(e) =>
+ if (interpreter.isReplDebug) e.printStackTrace()
+ interpreter.replinfo(s"Warning: history file ${p}'s permissions could not be restricted to owner-only.")
+ }
+
+ p
+ }
+
private var isPersistent = true
locally {
@@ -79,6 +102,5 @@ object FileBackedHistory {
// val ContinuationNL: String = Array('\003', '\n').mkString
import Properties.userHome
- def defaultFileName = ".scala_history"
- def defaultFile: File = File(Path(userHome) / defaultFileName)
+ final val defaultFileName = ".scala_history"
}
diff --git a/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala b/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala
index 1f6fa68f572..0673fa1f758 100644
--- a/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala
+++ b/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala
@@ -3,19 +3,18 @@
* @author Paul Phillips
*/
-package scala.tools.nsc
-package util
-
-import java.lang.{ ClassLoader => JClassLoader }
-import java.lang.reflect.{ Constructor, Modifier, Method }
-import java.io.{ File => JFile }
-import java.net.{ URLClassLoader => JURLClassLoader }
-import java.net.URL
-import scala.reflect.runtime.ReflectionUtils.unwrapHandler
-import ScalaClassLoader._
-import scala.util.control.Exception.{ catching }
+package scala.tools.nsc.util
+
+import java.io.{File => JFile}
+import java.lang.reflect.{Constructor, Modifier}
+import java.lang.{ClassLoader => JClassLoader}
+import java.net.{URL, URLClassLoader => JURLClassLoader}
+
import scala.language.implicitConversions
-import scala.reflect.{ ClassTag, classTag }
+import scala.reflect.runtime.ReflectionUtils.unwrapHandler
+import scala.reflect.{ClassTag, classTag}
+import scala.tools.nsc.io.Streamable
+import scala.util.control.Exception.catching
trait HasClassPath {
def classPathURLs: Seq[URL]
@@ -25,6 +24,8 @@ trait HasClassPath {
* of java reflection.
*/
trait ScalaClassLoader extends JClassLoader {
+ import ScalaClassLoader._
+
/** Executing an action with this classloader as context classloader */
def asContext[T](action: => T): T = {
val saved = contextLoader
@@ -52,7 +53,7 @@ trait ScalaClassLoader extends JClassLoader {
/** The actual bytes for a class file, or an empty array if it can't be found. */
def classBytes(className: String): Array[Byte] = classAsStream(className) match {
case null => Array()
- case stream => io.Streamable.bytes(stream)
+ case stream => Streamable.bytes(stream)
}
/** An InputStream representing the given class name, or null if not found. */
diff --git a/src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala b/src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala
new file mode 100644
index 00000000000..c0da65db387
--- /dev/null
+++ b/src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala
@@ -0,0 +1,107 @@
+/* NSC -- new Scala compiler
+ * Copyright 2017 LAMP/EPFL
+ * @author Martin Odersky
+ */
+package scala.reflect.internal.util
+
+import java.io.{File, FileOutputStream, IOException}
+
+
+trait OwnerOnlyChmod {
+ /** Remove group/other permissions for `file`, it if exists */
+ def chmod(file: java.io.File): Unit
+
+ /** Delete `file` if it exists, recreate it with no group/other permissions, and write `contents` */
+ final def chmodAndWrite(file: File, contents: Array[Byte]): Unit = {
+ file.delete()
+ val fos = new FileOutputStream(file)
+ fos.close()
+ chmod(file)
+ val fos2 = new FileOutputStream(file)
+ try {
+ fos2.write(contents)
+ } finally {
+ fos2.close()
+ }
+ }
+
+ // TODO: use appropriate NIO call instead of two-step exists?/create!
+ final def chmodOrCreateEmpty(file: File): Unit =
+ if (!file.exists()) chmodAndWrite(file, Array[Byte]()) else chmod(file)
+
+}
+
+object OwnerOnlyChmod {
+ def apply(): OwnerOnlyChmod = {
+ if (!util.Properties.isWin) Java6UnixChmod
+ else if (util.Properties.isJavaAtLeast("7")) new NioAclChmodReflective
+ else NoOpOwnerOnlyChmod
+ }
+}
+
+object NoOpOwnerOnlyChmod extends OwnerOnlyChmod {
+ override def chmod(file: File): Unit = ()
+}
+
+
+/** Adjust permissions with `File.{setReadable, setWritable}` */
+object Java6UnixChmod extends OwnerOnlyChmod {
+
+ def chmod(file: File): Unit = if (file.exists()) {
+ def clearAndSetOwnerOnly(f: (Boolean, Boolean) => Boolean): Unit = {
+ def fail() = throw new IOException("Unable to modify permissions of " + file)
+ // attribute = false, ownerOnly = false
+ if (!f(false, false)) fail()
+ // attribute = true, ownerOnly = true
+ if (!f(true, true)) fail()
+ }
+ if (file.isDirectory) {
+ clearAndSetOwnerOnly(file.setExecutable)
+ }
+ clearAndSetOwnerOnly(file.setReadable)
+ clearAndSetOwnerOnly(file.setWritable)
+ }
+}
+
+
+object NioAclChmodReflective {
+ val file_toPath = classOf[java.io.File].getMethod("toPath")
+ val files = Class.forName("java.nio.file.Files")
+ val path_class = Class.forName("java.nio.file.Path")
+ val getFileAttributeView = files.getMethod("getFileAttributeView", path_class, classOf[Class[_]], Class.forName("[Ljava.nio.file.LinkOption;"))
+ val linkOptionEmptyArray = java.lang.reflect.Array.newInstance(Class.forName("java.nio.file.LinkOption"), 0)
+ val aclFileAttributeView_class = Class.forName("java.nio.file.attribute.AclFileAttributeView")
+ val aclEntry_class = Class.forName("java.nio.file.attribute.AclEntry")
+ val aclEntryBuilder_class = Class.forName("java.nio.file.attribute.AclEntry$Builder")
+ val newBuilder = aclEntry_class.getMethod("newBuilder")
+ val aclEntryBuilder_build = aclEntryBuilder_class.getMethod("build")
+ val userPrinciple_class = Class.forName("java.nio.file.attribute.UserPrincipal")
+ val setPrincipal = aclEntryBuilder_class.getMethod("setPrincipal", userPrinciple_class)
+ val setPermissions = aclEntryBuilder_class.getMethod("setPermissions", Class.forName("[Ljava.nio.file.attribute.AclEntryPermission;"))
+ val aclEntryType_class = Class.forName("java.nio.file.attribute.AclEntryType")
+ val setType = aclEntryBuilder_class.getMethod("setType", aclEntryType_class)
+ val aclEntryPermission_class = Class.forName("java.nio.file.attribute.AclEntryPermission")
+ val aclEntryPermissionValues = aclEntryPermission_class.getDeclaredMethod("values")
+ val aclEntryType_ALLOW = aclEntryType_class.getDeclaredField("ALLOW")
+}
+
+/** Reflective version of `NioAclChmod` */
+final class NioAclChmodReflective extends OwnerOnlyChmod {
+ import NioAclChmodReflective._
+ def chmod(file: java.io.File): Unit = {
+ val path = file_toPath.invoke(file)
+ val view = getFileAttributeView.invoke(null, path, aclFileAttributeView_class, linkOptionEmptyArray)
+ val setAcl = aclFileAttributeView_class.getMethod("setAcl", classOf[java.util.List[_]])
+ val getOwner = aclFileAttributeView_class.getMethod("getOwner")
+ val owner = getOwner.invoke(view)
+ setAcl.invoke(view, acls(owner))
+ }
+
+ private def acls(owner: Object) = {
+ val builder = newBuilder.invoke(null)
+ setPrincipal.invoke(builder, owner)
+ setPermissions.invoke(builder, aclEntryPermissionValues.invoke(null))
+ setType.invoke(builder, aclEntryType_ALLOW.get(null))
+ java.util.Collections.singletonList(aclEntryBuilder_build.invoke(builder))
+ }
+}