Summary
ClassLoaderClassWriter.getCommonSuperClass returns java/lang/Object whenever it cannot read a type's class bytes / reflectively resolve it. That is a silent failure mode: ASM then writes incorrect stack map frames and the JVM rejects the patched class with VerifyError.
JarProcessor.process compounds this by building a URLClassLoader over only the input JAR (parented to sloth's own classloader). Hierarchy types that live in other classpath entries are invisible, so any merge across those types collapses to Object.
Impact (Scala CLI / GraalVM native image)
Scala CLI drives sloth to patch Scala 3.3.x compiler and library jars under --sloth. On the JVM launcher this often appears to work because the parent classloader accidentally sees scala-library (and HotSpot can serve JDK .class resources). Inside a GraalVM native image:
- There are no
.class resources at all → getResourceAsStream fails for JDK types.
Class.forName(name, false, customLoader) also fails to resolve JDK types reliably.
- Every reference-type merge in patched methods (e.g.
FileZipArchive.openZipFile merging JarFile and ZipFile) becomes Object → VerifyError: Bad return type.
We hit this on scala3-compiler_3-3.3.8.jar (dotty.tools.io.FileZipArchive.openZipFile) when starting scala-cli --power repl --sloth -S 3.3.8 with the native launcher.
Suggested fixes
- Do not silently fall back to
Object in ClassLoaderClassWriter.getCommonSuperClass when a type cannot be resolved — fail loudly (or at least log) so callers notice corrupt frames.
- Let
JarProcessor.process accept a caller-provided ClassLoader / resolver (or an explicit hierarchy classpath) instead of always building a URLClassLoader over only the input JAR.
Workaround
Scala CLI now reimplements jar processing on top of LazyValAnalyzer / BytecodePatcher with a classpath-backed resolver that serves jar/dir bytes via ZipFile / file I/O and synthesizes minimal JDK classfile stubs (superclass + interfaces) for native-image-safe frame computation.
Additional notes
Found while working on VirtusLab/scala-cli#4404
Summary
ClassLoaderClassWriter.getCommonSuperClassreturnsjava/lang/Objectwhenever it cannot read a type's class bytes / reflectively resolve it. That is a silent failure mode: ASM then writes incorrect stack map frames and the JVM rejects the patched class withVerifyError.JarProcessor.processcompounds this by building aURLClassLoaderover only the input JAR (parented to sloth's own classloader). Hierarchy types that live in other classpath entries are invisible, so any merge across those types collapses toObject.Impact (Scala CLI / GraalVM native image)
Scala CLI drives sloth to patch Scala 3.3.x compiler and library jars under
--sloth. On the JVM launcher this often appears to work because the parent classloader accidentally seesscala-library(and HotSpot can serve JDK.classresources). Inside a GraalVM native image:.classresources at all →getResourceAsStreamfails for JDK types.Class.forName(name, false, customLoader)also fails to resolve JDK types reliably.FileZipArchive.openZipFilemergingJarFileandZipFile) becomesObject→VerifyError: Bad return type.We hit this on
scala3-compiler_3-3.3.8.jar(dotty.tools.io.FileZipArchive.openZipFile) when startingscala-cli --power repl --sloth -S 3.3.8with the native launcher.Suggested fixes
ObjectinClassLoaderClassWriter.getCommonSuperClasswhen a type cannot be resolved — fail loudly (or at least log) so callers notice corrupt frames.JarProcessor.processaccept a caller-providedClassLoader/ resolver (or an explicit hierarchy classpath) instead of always building aURLClassLoaderover only the input JAR.Workaround
Scala CLI now reimplements jar processing on top of
LazyValAnalyzer/BytecodePatcherwith a classpath-backed resolver that serves jar/dir bytes viaZipFile/ file I/O and synthesizes minimal JDK classfile stubs (superclass + interfaces) for native-image-safe frame computation.Additional notes
Found while working on VirtusLab/scala-cli#4404