Skip to content

Fix for #22461 Empty ClassPath attribute in one or more classpath jars causes crash #22462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 21, 2025

Conversation

philwalk
Copy link
Contributor

@philwalk philwalk commented Jan 26, 2025

Change to how an empty or null jar manifest ClassPath: property is handled:

in dotty.tools.dotc.classpath.ClassPathFactory:

  • classesInExpandedPath(...) returns and empty IndexedSeq rather than crash
  • isJarOrZip returns false on a null reference rather than crash

in dotty.tools.dotc.classpath.FileUtils:

  • createSourcePath fails with an error message on a null file parameter.

In the context of #22461, this causes an empty ClassPath: property to be treated the same as a mispelled or missing classpath entry, which are silently ignored, matching the behaviour of legacy scripts.

Closes #22461

@som-snytt
Copy link
Contributor

I was interested in trying it out because this is more-or-less shared code with Scala 2. I did not yet reproduce.

I would not expect to handle a possible null in more than one place. Dotty is usually complaining to me that such-and-such might be null, I have to sprinkle | Null if I think that is desired.

@philwalk
Copy link
Contributor Author

philwalk commented Jan 28, 2025

I would not expect to handle a possible null in more than one place.

The critical change is here consisting of a null check inserted after line 71.
EDIT:
As shown in the stacktrace of #22461, an empty String is considered to be a file by if Files.exists(path)

The other changes don't address this bug, and are optional, intended to provide a more to-the-point error message.

Wow, I just noticed I uninitentionally commited a file related to #22443. I will revert that and reduce other changes to the absolute minimum.

@philwalk philwalk force-pushed the empty-classpath-crash branch from 4f49bc2 to 5e2bcf1 Compare January 28, 2025 20:59
@philwalk
Copy link
Contributor Author

philwalk commented Jan 28, 2025

The proposed changes are now reduced to the bare minimum necessary.
A hypothesis as to why the fix is necessary:

          if Files.exists(path)               // `path` is the empty String
          entry = AbstractFile.getFile(path)  // `entry` IS null (Windows only)

I will try to manually verify this

EDIT: the following script verifies the problem in Windows:

#!/usr/bin/env -S scalaQ
object AbstractFileTest {
  def main(args: Array[String]): Unit = {
    import dotty.tools.io.AbstractFile
    import java.nio.file.{Path, Paths, Files}
    val path = Paths.get("")
    printf("path [%s] exists : %s\n", path,  Files.exists(path))
    val entry = AbstractFile.getFile(path)
    printf("entry [%s]\n", entry)
  }
}
# ./abstractFileTest.sc
[warning] MainGenericRunner class is deprecated since Scala 3.5.0, and Scala CLI features will not work.
[warning] Please be sure to update to the Scala CLI launcher to use the new features.
[warning] Check the Scala 3.5.0 release notes to troubleshoot your installation.
path [] exists : true
entry [null]

EDIT:
So an alternate fix would be to replace ClassPathFactory line 71L

  if Files.exists(path)

with this:

  if path.toFile.exists

EDIT:
Although it would fix the problem, performance can be very bad when path doesn't exist, as I recall. If so, the best performance might be this:

  if path.trim.nonEmpty && Files.exists(path)

@philwalk
Copy link
Contributor Author

philwalk commented Jan 28, 2025

The JVM does the same thing in WSL as in Windows, so the reason this bug is Windows-only must be due to what AbstractFile does with an empty String Path.

# WSL
$ wsl /opt/scala/bin/scala -e 'printf("%s\n", java.nio.file.Files.exists(java.nio.file.Paths.get("")))'
true
# Windows
$ /opt/scala/bin/scala -e 'printf("%s\n", java.nio.file.Files.exists(java.nio.file.Paths.get("")))'
true

@philwalk
Copy link
Contributor Author

To my surprise, the problem exists also in WSL, at least implied by the result of the abstractFileTest.sc script.
Here's how I did the test in WSL:

cd ~/workspace/scala3
sbt dist/Universal/packageBin
export SCALA_HOME=`pwd -P`/dist/linux-x86_64/target/universal/stage
export PATH="$SCALA_HOME/bin:$PATH"

BTW, the universal distribution includes scala_legacy in the bin directory.
Here's the test script:

#!/usr/bin/env -S scala_legacy

object AbstractFileTest {
  def main(args: Array[String]): Unit = {
    import dotty.tools.io.AbstractFile
    import java.nio.file.{Path, Paths, Files}
    val path = Paths.get("")
    printf("path [%s] exists : %s\n", path,  Files.exists(path))
    val entry = AbstractFile.getFile(path)
    printf("entry [%s]\n", entry)
  }
}

and here's the output:

(base) philwalk@d5:~/workspace/scala3$ ./abstractFileTest.sc
[warning] MainGenericRunner class is deprecated since Scala 3.5.0, and Scala CLI features will not work.
[warning] Please be sure to update to the Scala CLI launcher to use the new features.
[warning] Check the Scala 3.5.0 release notes to troubleshoot your installation.
path [] exists : true
entry [null]

@som-snytt
Copy link
Contributor

FYI the equivalent code on Scala 2 was changed to test !isDirectory instead of isFile. I even touched ScriptRunner! by coincidence.

scala/scala#6365

I see there was one ancient attempt to improve the code in dotty, but I understand the assumption was that it would be replaced altogether at some point. I assume no one would object to any change you deem useful.

I don't know whether this is useful information.

➜  cat af-test.scala

object AbstractFileTest {
  def main(args: Array[String]): Unit = {
    //import dotty.tools.io.AbstractFile
    import scala.reflect.io.{AbstractFile, Path}
    import java.nio.file.{Paths, Files}
    val path = Paths.get("")
    printf("path [%s] exists : %s\n", path,  Files.exists(path))
    val x = Path(path.toFile)
    val entry = AbstractFile.getFile(x)
    printf("entry [%s]\n", entry)
  }
}
➜  scala af-test.scala
path [] exists : true
entry []

@hamzaremmal
Copy link
Member

@philwalk Can you please drop the first commit and adjust the second ? The first commit has nothing to do this the original issue, and reverting it's change will only cause conflicts with your other PR when merging.

Copy link
Member

@hamzaremmal hamzaremmal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest making the change here instead:

for (m <- manifest ; cp <- m.attrs.get(Name.CLASS_PATH)) yield cp

def classPathString: Option[String] =
  for (m <- manifest ; cp <- m.attrs.get(Name.CLASS_PATH) if !cp.isBlank()) yield cp

@hamzaremmal hamzaremmal assigned philwalk and unassigned hamzaremmal Feb 21, 2025
@philwalk
Copy link
Contributor Author

@philwalk Can you please drop the first commit and adjust the second ? The first commit has nothing to do this the original issue, and reverting it's change will only cause conflicts with your other PR when merging.

Looking into it ...

@som-snytt
Copy link
Contributor

I commented on the ticket that the empty attribute is out of spec, so I like the clever idea of pushing the filter down to Jar.

@philwalk
Copy link
Contributor Author

@philwalk Can you please drop the first commit and adjust the second ? The first commit has nothing to do this the original issue, and reverting it's change will only cause conflicts with your other PR when merging.

It's sounds like I can revert both commits and make your suggested change to Jar.scala, does that sound right?

@hamzaremmal
Copy link
Member

@philwalk Can you please drop the first commit and adjust the second ? The first commit has nothing to do this the original issue, and reverting it's change will only cause conflicts with your other PR when merging.

It's sounds like I can revert both commits and make your suggested change to Jar.scala, does that sound right?

Yes, just make sure to have a single commit please

@philwalk
Copy link
Contributor Author

philwalk commented Feb 21, 2025

Also, just to be clear, this will revert the change to dist/libexec/common-shared, undoing a needed (but unrelated) fix, but with the intent of creating a new PR for that fix later?

@hamzaremmal
Copy link
Member

Also, just to be clear, this will revert the change to dist/libexec/common-shared, undoing a needed (but unrelated) fix, but with the intent of creating a new PR for that fix later?

There is already a PR for that change: #22444

@philwalk philwalk force-pushed the empty-classpath-crash branch from 5e2bcf1 to f24b5d1 Compare February 21, 2025 18:27
@hamzaremmal hamzaremmal merged commit 1279286 into scala:main Feb 21, 2025
29 checks passed
@philwalk philwalk deleted the empty-classpath-crash branch February 22, 2025 13:42
@WojciechMazur WojciechMazur added this to the 3.7.0 milestone Mar 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

scala_legacy crash caused by any jar in classpath with empty Class-Path: attribute
4 participants