Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8331467

ImageReaderFactory can cause a ClassNotFoundException if the default FileSystemProvider is not the system-default provider

XMLWordPrintable

      ADDITIONAL SYSTEM INFORMATION :
      OS: windows_11
      Java Runtime Information:
      openjdk version "22.0.1" 2024-04-16
      OpenJDK Runtime Environment (build 22.0.1+8-16)
      OpenJDK 64-Bit Server VM (build 22.0.1+8-16, mixed mode)
      maven info:
      Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
      Java version: 22.0.1, vendor: Oracle Corporation, runtime: jdk-22
      platform encoding: UTF-8
      OS name: "windows 11", version: "10.0", arch: "amd64", family: "windows"

      A DESCRIPTION OF THE PROBLEM :
      When a custom FileSystemProvider is set by "-Djava.nio.file.spi.DefaultFileSystemProvider=org.example.MyFileSystemProvider" on VM options and the code is running in the custom jrt which is build by jmod and jlink tool the circular dependency between FileSystem and ImageReaderFactory will cause CNFE of the org.example.MyFileSystemProvider.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1.compile and package the source code by maven: `mvn clean package`
      2. create jmod by `jmod create --class-path ./demo-1.0-SNAPSHOT.jar demo.jmode`
      3. jlink by `jlink --output ./myjre --module-path "demo.jmod;$(JAVA_HOME)/jmods" --add-modules ALL-MODULE-PATH`
      4. run `myjre/bin/java "-Djava.nio.file.spi.DefaultFileSystemProvider=org.example.MyFileSystemProvider" -m demo`

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      org.example.MyFileSystemProvider will be set successfully and there is no exception.
      ACTUAL -
      CNFE:
      Error: A JNI error has occurred, please check your installation and try again
      Exception in thread "main" java.lang.Error: java.lang.ClassNotFoundException: org.example.MyFileSystemProvider
              at java.base/java.nio.file.FileSystems$DefaultFileSystemHolder.getDefaultProvider(FileSystems.java:134)
              at java.base/java.nio.file.FileSystems$DefaultFileSystemHolder$1.run(FileSystems.java:103)
              at java.base/java.nio.file.FileSystems$DefaultFileSystemHolder$1.run(FileSystems.java:101)
              at java.base/java.security.AccessController.doPrivileged(AccessController.java:319)
              at java.base/java.nio.file.FileSystems$DefaultFileSystemHolder.defaultFileSystem(FileSystems.java:101)
              at java.base/java.nio.file.FileSystems$DefaultFileSystemHolder.<clinit>(FileSystems.java:94)
              at java.base/java.nio.file.FileSystems.getDefault(FileSystems.java:183)
              at java.base/java.nio.file.Path.of(Path.java:148)
              at java.base/java.nio.file.Paths.get(Paths.java:69)
              at java.base/jdk.internal.jimage.ImageReaderFactory.<clinit>(ImageReaderFactory.java:51)
              at java.base/jdk.internal.module.SystemModuleFinders$SystemImage.<clinit>(SystemModuleFinders.java:385)
              at java.base/jdk.internal.module.SystemModuleFinders$SystemModuleReader.findImageLocation(SystemModuleFinders.java:430)
              at java.base/jdk.internal.module.SystemModuleFinders$SystemModuleReader.read(SystemModuleFinders.java:485)
              at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:814)
              at java.base/jdk.internal.loader.BuiltinClassLoader.findClassInModuleOrNull(BuiltinClassLoader.java:745)
              at java.base/jdk.internal.loader.BuiltinClassLoader.findClass(BuiltinClassLoader.java:622)
              at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:637)
              at java.base/java.lang.Class.forName(Class.java:620)
              at java.base/java.lang.Class.forName(Class.java:595)
              at java.base/sun.launcher.LauncherHelper.loadModuleMainClass(LauncherHelper.java:798)
              at java.base/sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:733)
      Caused by: java.lang.ClassNotFoundException: org.example.MyFileSystemProvider
              at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:642)
              at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
              at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
              at java.base/java.lang.Class.forName0(Native Method)
              at java.base/java.lang.Class.forName(Class.java:529)
              at java.base/java.lang.Class.forName(Class.java:508)
              at java.base/java.nio.file.FileSystems$DefaultFileSystemHolder.getDefaultProvider(FileSystems.java:124)
              ... 20 more

      ---------- BEGIN SOURCE ----------
      MyFileSystemProvider.java:
      ```java
      package org.example;

      import java.io.IOException;
      import java.net.URI;
      import java.nio.channels.SeekableByteChannel;
      import java.nio.file.*;
      import java.nio.file.attribute.BasicFileAttributes;
      import java.nio.file.attribute.FileAttribute;
      import java.nio.file.attribute.FileAttributeView;
      import java.nio.file.spi.FileSystemProvider;
      import java.util.Map;
      import java.util.Set;

      public class MyFileSystemProvider extends FileSystemProvider {

          private final FileSystemProvider fileSystemProvider;

          public MyFileSystemProvider(FileSystemProvider fileSystemProvider) {
              this.fileSystemProvider = fileSystemProvider;
          }

          @Override
          public String getScheme() {
              return fileSystemProvider.getScheme();
          }

          @Override
          public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
              return fileSystemProvider.newFileSystem(uri, env);
          }

          @Override
          public FileSystem getFileSystem(URI uri) {
              return fileSystemProvider.getFileSystem(uri);
          }

          @Override
          public Path getPath(URI uri) {
              return fileSystemProvider.getPath(uri);
          }

          @Override
          public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
              return fileSystemProvider.newByteChannel(path, options, attrs);
          }

          @Override
          public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
              return fileSystemProvider.newDirectoryStream(dir, filter);
          }

          @Override
          public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
              fileSystemProvider.createDirectory(dir, attrs);
          }

          @Override
          public void delete(Path path) throws IOException {
              fileSystemProvider.delete(path);
          }

          @Override
          public void copy(Path source, Path target, CopyOption... options) throws IOException {
              fileSystemProvider.copy(source, target, options);
          }

          @Override
          public void move(Path source, Path target, CopyOption... options) throws IOException {
              fileSystemProvider.move(source, target, options);
          }

          @Override
          public boolean isSameFile(Path path, Path path2) throws IOException {
              return fileSystemProvider.isSameFile(path, path2);
          }

          @Override
          public boolean isHidden(Path path) throws IOException {
              return fileSystemProvider.isHidden(path);
          }

          @Override
          public FileStore getFileStore(Path path) throws IOException {
              return fileSystemProvider.getFileStore(path);
          }

          @Override
          public void checkAccess(Path path, AccessMode... modes) throws IOException {
              fileSystemProvider.checkAccess(path, modes);
          }

          @Override
          public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
              return fileSystemProvider.getFileAttributeView(path, type, options);
          }

          @Override
          public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
              return fileSystemProvider.readAttributes(path, type, options);
          }

          @Override
          public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
              return fileSystemProvider.readAttributes(path, attributes, options);
          }

          @Override
          public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
              fileSystemProvider.setAttribute(path, attribute, value, options);
          }
      }
      ```
      Main.java:
      ```java
      package org.example;

      public class Main {
          public static void main(String[] args){
                System.out.println("success");
          }
      }
      ```
      module-info.java:
      ```java
      module demo {
          requires java.base;
          exports org.example;
      }
      ```
      pom.xml:
      ```xml
      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>

          <groupId>org.example</groupId>
          <artifactId>demo</artifactId>
          <version>1.0-SNAPSHOT</version>

          <properties>
              <maven.compiler.source>22</maven.compiler.source>
              <maven.compiler.target>22</maven.compiler.target>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          </properties>

          <build>
              <plugins>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-compiler-plugin</artifactId>
                      <version>3.11.0</version>
                      <configuration>
                          <source>22</source>
                          <target>22</target>
                          <encoding>UTF-8</encoding>
                          <skip>true</skip>
                      </configuration>
                  </plugin>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-jar-plugin</artifactId>
                      <version>3.2.2</version>
                      <configuration>
                          <archive>
                              <manifest>
                                  <mainClass>org.example.Main</mainClass>
                              </manifest>
                          </archive>
                      </configuration>
                  </plugin>
              </plugins>
          </build>

      </project>
      ```
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      The reason for this bug is the same as [that NPE bug](https://bugs.openjdk.org/browse/JDK-8263940?jql=text%20~%20%22java.nio.file.spi.DefaultFileSystemProvider%22), both caused by circular dependencies during class loading resulting in field values being initialized to NULL. The difference from JDK-8263940 is that JDK-8263940 directly manifests as NPE, while in this bug, it manifests as CNFE.
      In SystemModuleFinders$SystemModuleReader.java
      ```java
      @Override
              public Optional<ByteBuffer> read(String name) throws IOException {
                  // location returned by findImageLocation will be NULL
                  ImageLocation location = findImageLocation(name);
                  if (location != null) {
                      return Optional.of(SystemImage.reader().getResourceBuffer(location));
                  } else {
                      return Optional.empty();
                  }
              }
      ```
      In findImageLocation method:
      ```java
      private ImageLocation findImageLocation(String name) throws IOException {
                  Objects.requireNonNull(name);
                  if (closed)
                      throw new IOException("ModuleReader is closed");
                  // SystemImage will be initialized for the first time when loading the Main class which can be seen in the error stack.And then to initialize ImageReaderFactory.Then pay attention to the `private static final Path BOOT_MODULES_JIMAGE`, this field will call Paths which will also call FileSystem#getDefault.The getDefault method will load the `org.example.MyFileSystemProvider` in which code will run to here again! But now we are still in the process of initializing the `private static final Path BOOT_MODULES_JIMAGE` field. Therefor imageReader will be NULL finally.
                  ImageReader imageReader = SystemImage.reader();
                  if (imageReader != null) {
                      return imageReader.findLocation(module, name);
                  } else {
                      // not an images build
                      return null;
                  }
              }
      ```
      A simple approach would be to follow the same method as JDK-8263940:
      In ImageReaderFactory:
      change `Paths.get` call to `DefaultFileSystemProvider.theFileSystem().getPath()`:
      ```java
      private static final Path BOOT_MODULES_JIMAGE = DefaultFileSystemProvider.theFileSystem().getPath(JAVA_HOME, "lib", "modules");
      ```

      FREQUENCY : always


            jpai Jaikiran Pai
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: