Graal: Nonexistent relative classpath resource appears to exist in native image

Created on 25 Mar 2020  路  2Comments  路  Source: oracle/graal

Background

Spring Framework provides a Resource abstraction for classpath resources that supports creation of relative resources. The Resource abstraction also provides support to determine if any given Resource _exists_ based on various techniques for detecting the presence of such a resource. For a Spring UrlResource, the exists() method is inherited from AbstractFileResolvingResource as can be seen below.

https://github.com/spring-projects/spring-framework/blob/9fb614a5c60bf6f269edee6b69e543d8ca6af1c3/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java#L46-L86

The NonexistentClasspathResource test case provided in this issue reproduces a bug detected in the Spring Framework while running within a native image.

Overview

If a _relative_ URL for a nonexistent resource is created based on an existing classpath resource within a native image, the nonexistent resource appears to exist within the native image even though it is not present.

For example, given the NonexistentClasspathResource test case below, the output when using a standard JDK results in the following.

+ SUCCESS: found classpath resource: NonexistentClasspathResource.class
+ getContentLengthLong() for file:/nonexistent_classpath_resource/NonexistentClasspathResource.class: 466
+ SUCCESS: classpath resource exists: file:/nonexistent_classpath_resource/NonexistentClasspathResource.class
+ SUCCESS: bogus classpath resource does not exist: file:/nonexistent_classpath_resource/Bogus.class

Whereas, if you compile the test case into a native image, the output is the following.

- FAILURE: could not find classpath resource: NonexistentClasspathResource.class

If you execute the test case with the GraalVM native image agent, it generates the following in resource-config.json:

{
  "resources":[
    {"pattern":"\\QMETA-INF/services/jdk.vm.ci.hotspot.HotSpotJVMCIBackendFactory\\E"}, 
    {"pattern":"\\QMETA-INF/services/jdk.vm.ci.services.JVMCIServiceLocator\\E"}, 
    {"pattern":"\\QNonexistentClasspathResource.class\\E"}
  ],
  "bundles":[]
}

If you then build the native image with the configuration generated by the agent, the output is the following.

+ SUCCESS: found classpath resource: NonexistentClasspathResource.class
+ getContentLengthLong() for resource:NonexistentClasspathResource.class: 466
+ SUCCESS: classpath resource exists: resource:NonexistentClasspathResource.class
- getContentLengthLong() for resource:Bogus.class: 466
- FAILURE: bogus classpath resource appears to exist: resource:Bogus.class

The above FAILURE points out the bug within a native image.

The first resource is now found; however, the second "bogus" resource also appears to exist within the native image. Note that java.net.URLConnection.getContentLengthLong() returns a positive value for the nonexistent resource that is identical to the value returned for the existing resource (466).

See https://github.com/sbrannen/graalvm-playground/tree/master/nonexistent_classpath_resource for full details.

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

public class NonexistentClasspathResource {

    public static void main(String[] args) throws Exception {
        new Tester().test();
    }


    static class Tester {

        void test() throws Exception {
            String name = "NonexistentClasspathResource.class";
            URL resource1 = getClass().getResource(name);
            if (resource1 == null) {
                System.err.println("FAILURE: could not find classpath resource: " + name);
                return;
            }
            System.err.println("SUCCESS: found classpath resource: " + name);

            if (!exists(resource1)) {
                System.err.println("FAILURE: classpath resource does not exist: " + resource1);
                return;
            }
            System.err.println("SUCCESS: classpath resource exists: " + resource1);

            URL resource2 = new URL(resource1, "Bogus.class");
            if (exists(resource2)) {
                System.err.println("FAILURE: bogus classpath resource appears to exist: " + resource2);
                return;
            }
            System.err.println("SUCCESS: bogus classpath resource does not exist: " + resource2);
        }

        private boolean exists(URL url) {
            try {
                // Try a URL connection content-length header
                URLConnection con = url.openConnection();
                if (con.getContentLengthLong() > 0) {
                    System.err.format("getContentLengthLong() for %s: %d%n", url, con.getContentLengthLong());
                    return true;
                }

                // Fall back to stream existence: can we open the stream?
                getInputStream(url).close();
                System.err.println("opened InputStream for " + url);
                return true;
            }
            catch (IOException ex) {
                return false;
            }
        }

        private InputStream getInputStream(URL url) throws IOException {
            return url.openConnection().getInputStream();
        }

    }

}

Environment

  • GraalVM version: CE 20.0
  • JDK major version: 8
  • OS: macOS 10.14.6
  • Architecture: i386 / x86_64
bug native-image spring

Most helpful comment

The resource URL handling mechanism in native image uses this construct[1]. So each registered resource is backed by a URLStreamHandler which creates a ConnURL for _that specific_ resource in openConnection (i.e. there's an implicit assumption that the URLStreamHandler instance will always only be called for that specific resource).

In the test case above, the following construct is used:

URL resource1 = getClass().getResource(name);
....
URL resource2 = new URL(resource1, "Bogus.class");


So a new URL (resource2) is constructed using the URL(URL context, String spec) constructor, which as per its javadoc will reuse the URLStreamHandler instance of the context URL (which in this case happens to be the URLStreamHandler of a resource that has been explicitly added to the native-image) and thus incorrectly returns the data of the registered resource.

[1] https://github.com/oracle/graal/blob/master/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java#L132

All 2 comments

The resource URL handling mechanism in native image uses this construct[1]. So each registered resource is backed by a URLStreamHandler which creates a ConnURL for _that specific_ resource in openConnection (i.e. there's an implicit assumption that the URLStreamHandler instance will always only be called for that specific resource).

In the test case above, the following construct is used:

URL resource1 = getClass().getResource(name);
....
URL resource2 = new URL(resource1, "Bogus.class");


So a new URL (resource2) is constructed using the URL(URL context, String spec) constructor, which as per its javadoc will reuse the URLStreamHandler instance of the context URL (which in this case happens to be the URLStreamHandler of a resource that has been explicitly added to the native-image) and thus incorrectly returns the data of the registered resource.

[1] https://github.com/oracle/graal/blob/master/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java#L132

@jaikiran, thanks for the detective work!

That certainly explains the odd behavior, and hopefully that will help the team come up with a fix.

Was this page helpful?
0 / 5 - 0 ratings