[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:
BrianHotopp
2026-06-13 01:54:58 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent 6ec1f742ab
commit cb90a47ce8
5 changed files with 51 additions and 2 deletions
+1 -1
View File
@@ -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
+24 -1
View File
@@ -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