Bug 58692 - Odd classpath URLs cause Tomcat to abort loading webapps
Summary: Odd classpath URLs cause Tomcat to abort loading webapps
Status: RESOLVED FIXED
Alias: None
Product: Tomcat 8
Classification: Unclassified
Component: Util (show other bugs)
Version: 8.0.x-trunk
Hardware: All All
: P2 normal (vote)
Target Milestone: ----
Assignee: Tomcat Developers Mailing List
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2015-12-04 23:40 UTC by Derek Abdine
Modified: 2015-12-16 21:03 UTC (History)
0 users



Attachments
Test class for StandardJarScanner (3.83 KB, text/x-csrc)
2015-12-04 23:40 UTC, Derek Abdine
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Derek Abdine 2015-12-04 23:40:41 UTC
Created attachment 33326 [details]
Test class for StandardJarScanner

An application was created with Tomcat 8 and Apache Felix both embedded on the same JVM. Apache felix happens to add a URL to the application classloader's classpath via it's extension manager, which is used to support bundle fragment/extension features of the OSGi spec:

https://github.com/apache/felix/blob/ac5ec40ee40ec92dc8124e22afa855ab2c4a850f/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java#L106

When Tomcat is started on the same JVM and a context is created, Tomcat will attempt to perform a jar scan on the classloader's classpath. It will call getURLs to enumerate all classpath URLs and peek inside for pluggability purposes:

https://github.com/apache/tomcat/blob/trunk/java/org/apache/tomcat/util/scan/StandardJarScanner.java#L221

For each URL enumerated from the classloader Tomcat attempts to transform it into a ClassPathEntry. For the special URL that Apache Felix adds, ClassPathEntry's getName method will return the empty string, as there is no file part in the URL itself:

https://github.com/apache/tomcat/blob/trunk/java/org/apache/tomcat/util/scan/StandardJarScanner.java#L353

There is no way to prevent this issue with a JarScanFilter, since the check method called here will use the empty string file name, which can't be matched using the glob matching algorithm:

https://github.com/apache/tomcat/blob/trunk/java/org/apache/tomcat/util/scan/StandardJarScanner.java#L237

Eventually this falls through to the process() method through this call:
https://github.com/apache/tomcat/blob/trunk/java/org/apache/tomcat/util/scan/StandardJarScanner.java#L244

Since the URL "http://felix.extensions:9/" has a compatible scheme (http) but no file part (it doesn't match the jar extension) we fall through to the else condition in the process method, which will attempt to call new File(new URL("http://felix.extensions:9")) and fail, because the File(URL) constructor requires that the URL start with "file:/":

https://github.com/apache/tomcat/blob/trunk/java/org/apache/tomcat/util/scan/StandardJarScanner.java#L317

This has the effect of bailing out the entire webapp loading.

Steps to reproduce:
I've created a test case which demonstrates the bug. I added this test case to TestStandardJarScanner to validate against 9.0 TRUNK, though the original bug was found on tomcat 8x.

For convenience i've attached a copy of the test class with this test case embedded in it.

    /**
     * Tomcat should ignore URLs which do not have a file part and do not use the file scheme.
     */
    @Test
    public void skipsInvalidClasspathURLNoFilePartNoFileScheme() {
        StandardJarScanner scanner = new StandardJarScanner();
        LoggingCallback callback = new LoggingCallback();
        TesterServletContext context = new TesterServletContext() {
            @Override
            public ClassLoader getClassLoader() {
                URLClassLoader urlClassLoader;
                try
                {
                    urlClassLoader = new URLClassLoader(new URL[] { new URL("http://felix.extensions:9/") });
                }
                catch (MalformedURLException e)
                {
                    throw new RuntimeException(e);
                }
                return urlClassLoader;
            }
        };
        scanner.scan(JarScanType.PLUGGABILITY, context, callback);
    }

WORKAROUNDS:
1. Disable classpath scanning in context.xml. This is a bit cumbersome for war files which contain their own context.xml as it requires a rebuild of all those downstream modules.
2. Add a LifecycleListener to hook into context creation events before they are initialized and set the jar filter to ignore empty string file names (due to the missing file name part of the class path entry).
Comment 1 Derek Abdine 2015-12-04 23:43:00 UTC
Here's the stack trace from the attached test case / unit test:
java.lang.IllegalArgumentException: URI scheme is not "file"
	at java.io.File.<init>(File.java:421)
	at org.apache.tomcat.util.scan.StandardJarScanner.process(StandardJarScanner.java:317)
	at org.apache.tomcat.util.scan.StandardJarScanner.scan(StandardJarScanner.java:244)
	at org.apache.tomcat.util.scan.TestStandardJarScanner.test(TestStandardJarScanner.java:83)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:483)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Comment 2 Derek Abdine 2015-12-04 23:44:46 UTC
Just a slight correction: I mentioned in the description

   "There is no way to prevent this issue with a JarScanFilter"

Just to clarify, there is no way to prevent this with the built in StandardJarScanFilter. As the workaround states, you can subclass and create your own to do this.
Comment 3 Mark Thomas 2015-12-16 21:03:22 UTC
Thanks for the report and the test case. It made investigating this really simple.

The fix and test case have been applied to 9.0.x and 8.0.x. Just the fix was applied to 7.0.x due to the refactoring of StandardJarScanner that took place between 7.0.x and 8.0.x.