mirror of
https://github.com/sbt/sbt.git
synced 2026-09-07 02:55:23 +02:00
[1.x] fix: Delegate JDK platform classes to the platform loader in console (#9327)
The console/consoleQuick REPL loader rejects JDK platform-module classes: ClasspathFilter's post-check refuses classes whose code source is a jrt: URL, so a top-down REPL reference to java.sql.* falls through to the REPL's own classloader, which finds the bytes in the runner-generated java9-rt-ext jar (via scala.ext.dirs) and dies in defineClass: java.lang.SecurityException: Prohibited package name: java.sql Wrap the REPL parent loader with a fallback that consults ClassLoader.getPlatformClassLoader (reflectively; absent on JDK 8, which has no platform loader) only after the existing chain misses, so project classes keep winning and platform classes resolve instead of being redefined. Linkage through the classpath loader (a JDBC driver implementing java.sql.Driver) was already sound: the launcher's topLoader delegates to the platform loader on JDK 9+; only the top-down path through ClasspathFilter was broken. Verified against sbt 1.11.7 (fails) and a patched 1.12.3-SNAPSHOT (passes) with the original report's repro: java.sql.Timestamp, a project class extending it, and DriverManager.getConnection with the postgresql driver, which now reaches the network layer. The console/* scripted group is added to CI; it was not in any scripted glob, so the new test (and the pre-existing console/project-compiler-bridge) never ran. Fixes #4328 (still reproducible on 1.11.7 before this change). Co-authored-by: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6ec1f742ab
commit
cb90a47ce8
@@ -97,7 +97,7 @@ jobs:
|
||||
if: ${{ matrix.jobtype == 2 }}
|
||||
shell: bash
|
||||
run: |
|
||||
./sbt -v "scripted actions/* apiinfo/* compiler-project/* ivy-deps-management/* reporter/* tests/* watch/* classloader-cache/* package/*"
|
||||
./sbt -v "scripted actions/* apiinfo/* compiler-project/* console/* ivy-deps-management/* reporter/* tests/* watch/* classloader-cache/* package/*"
|
||||
- name: Build and test (3)
|
||||
if: ${{ matrix.jobtype == 3 }}
|
||||
shell: bash
|
||||
|
||||
@@ -2185,6 +2185,28 @@ object Defaults extends BuildCommon {
|
||||
|
||||
def consoleTask: Initialize[Task[Unit]] = consoleTask(fullClasspath, console)
|
||||
def consoleQuickTask = consoleTask(externalDependencyClasspath, consoleQuick)
|
||||
|
||||
// The ScalaInstance loader chain bottoms out at the launcher's loader, which does not
|
||||
// delegate to the JDK platform classloader. None on JDK 8 (no platform loader).
|
||||
private[this] lazy val platformLoader: Option[ClassLoader] =
|
||||
try {
|
||||
Option(
|
||||
classOf[ClassLoader]
|
||||
.getMethod("getPlatformClassLoader")
|
||||
.invoke(null)
|
||||
.asInstanceOf[ClassLoader]
|
||||
)
|
||||
} catch { case NonFatal(_) => None }
|
||||
|
||||
private[this] def platformFallbackLoader(parent: ClassLoader): ClassLoader =
|
||||
platformLoader match {
|
||||
case Some(platform) =>
|
||||
new ClassLoader(parent) {
|
||||
override def findClass(name: String): Class[_] = platform.loadClass(name)
|
||||
}
|
||||
case None => parent
|
||||
}
|
||||
|
||||
def consoleTask(classpath: TaskKey[Classpath], task: TaskKey[_]): Initialize[Task[Unit]] =
|
||||
Def.task {
|
||||
val si = (task / scalaInstance).value
|
||||
@@ -2192,7 +2214,8 @@ object Defaults extends BuildCommon {
|
||||
val cpFiles = data((task / classpath).value)
|
||||
val fullcp = (cpFiles ++ si.allJars).distinct
|
||||
val tempDir = IO.createUniqueDirectory((task / taskTemporaryDirectory).value).toPath
|
||||
val loader = ClasspathUtil.makeLoader(fullcp.map(_.toPath), si, tempDir)
|
||||
val loader =
|
||||
platformFallbackLoader(ClasspathUtil.makeLoader(fullcp.map(_.toPath), si, tempDir))
|
||||
val compiler =
|
||||
(task / compilers).value.scalac match {
|
||||
case ac: AnalyzingCompiler => ac.onArgs(exported(s, "scala"))
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
scalaVersion := "2.13.12"
|
||||
|
||||
lazy val markerFile = settingKey[java.io.File]("marker file written by the console REPL when JDK platform classes load")
|
||||
|
||||
markerFile := target.value / "console-jdk-platform-ok"
|
||||
|
||||
console / initialCommands := {
|
||||
val path = markerFile.value.getAbsolutePath.replace("\\", "\\\\")
|
||||
// java.sql lives in a JDK platform module. Loading it through the console REPL's
|
||||
// classloader chain threw SecurityException("Prohibited package name: java.sql")
|
||||
// before the platform-loader fallback (#4328): `ts` covers the top-down request,
|
||||
// `mts` covers supertype resolution for a class defined from the project classpath.
|
||||
// The REPL traps exceptions, so the assertion is a marker file: it is only written
|
||||
// if both classes actually load.
|
||||
s"""val ts = new java.sql.Timestamp(0L)
|
||||
|val mts = new MyTimestamp
|
||||
|java.nio.file.Files.write(java.nio.file.Paths.get("$path"), (ts.getClass.getName + " " + mts.getClass.getName).getBytes("UTF-8"))
|
||||
|""".stripMargin
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Linkage through the defining loader: resolving the java.sql.Timestamp supertype of a
|
||||
// project-classpath class exercises the classpath loader's parent chain, not a top-down
|
||||
// REPL request. This is the shape of sbt/sbt#4328's original repro (a JDBC driver
|
||||
// implementing java.sql.Driver).
|
||||
class MyTimestamp extends java.sql.Timestamp(0L)
|
||||
@@ -0,0 +1,2 @@
|
||||
> console
|
||||
$ exists target/console-jdk-platform-ok
|
||||
Reference in New Issue
Block a user