From dbb0aa15f6d7e2b681e4b8b87665779f1f6730da Mon Sep 17 00:00:00 2001 From: jmehrens Date: Fri, 5 Jan 2024 11:29:43 -0600 Subject: [PATCH] Jakarta Mail erroneously assumes that classes can be loaded from Thread#getContextClassLoader (#701) * Jakarta Mail erroneously assumes that classes can be loaded from Thread#getContextClassLoader Signed-off-by: jmehrens --- api/src/main/java/jakarta/mail/Session.java | 145 ++++++++++++------ .../java/jakarta/mail/util/FactoryFinder.java | 134 ++++++++++------ .../mail/util/DummyStreamProvider.java | 3 + .../jakarta/mail/util/FactoryFinderTest.java | 21 +++ doc/release/CHANGES.txt | 5 +- 5 files changed, 216 insertions(+), 92 deletions(-) diff --git a/api/src/main/java/jakarta/mail/Session.java b/api/src/main/java/jakarta/mail/Session.java index a0c58330..32224691 100644 --- a/api/src/main/java/jakarta/mail/Session.java +++ b/api/src/main/java/jakarta/mail/Session.java @@ -36,6 +36,7 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; @@ -255,7 +256,7 @@ private Session(Properties props, Authenticator authenticator) { this.authenticator = authenticator; this.streamProvider = StreamProvider.provider(); - if (Boolean.valueOf(props.getProperty("mail.debug")).booleanValue()) + if (Boolean.parseBoolean(props.getProperty("mail.debug"))) debug = true; initLogger(); @@ -982,21 +983,28 @@ public void load(InputStream is) throws IOException { } catch (SecurityException ex) { } + //Fetch classloader of given class, falling back to others if needed. + ClassLoader gcl; + ClassLoader[] loaders = getClassLoaders(cl, Thread.class, System.class); + if (loaders.length != 0) { + gcl = loaders[0]; + } else { + gcl = getContextClassLoader(); //Fail safe + } + // next, add all the non-default services - ServiceLoader sl = ServiceLoader.load(Provider.class); + ServiceLoader sl = ServiceLoader.load(Provider.class, gcl); for (Provider p : sl) { if (!containsDefaultProvider(p)) addProvider(p); } // + handle Glassfish/OSGi (platform specific default) - if (isHk2Available()) { - Iterator iter = lookupUsingHk2ServiceLoader(Provider.class.getName()); - while (iter.hasNext()) { - Provider p = iter.next(); - if (!containsDefaultProvider(p)) - addProvider(p); - } + Iterator iter = lookupUsingHk2ServiceLoader(Provider.class, gcl); + while (iter.hasNext()) { + Provider p = iter.next(); + if (!containsDefaultProvider(p)) + addProvider(p); } // load the META-INF/javamail.providers file supplied by an application @@ -1006,46 +1014,44 @@ public void load(InputStream is) throws IOException { loadResource("/META-INF/javamail.default.providers", cl, loader, false); // finally, add all the default services - sl = ServiceLoader.load(Provider.class); + sl = ServiceLoader.load(Provider.class, gcl); for (Provider p : sl) { if (containsDefaultProvider(p)) addProvider(p); } // + handle Glassfish/OSGi (platform specific default) - if (isHk2Available()) { - Iterator iter = lookupUsingHk2ServiceLoader(Provider.class.getName()); - while (iter.hasNext()) { - Provider p = iter.next(); - if (containsDefaultProvider(p)) { - addProvider(p); - } + iter = lookupUsingHk2ServiceLoader(Provider.class, gcl); + while (iter.hasNext()) { + Provider p = iter.next(); + if (containsDefaultProvider(p)) { + addProvider(p); } } /* * If we haven't loaded any providers, fake it. */ - if (providers.size() == 0) { + if (providers.isEmpty()) { logger.config("failed to load any providers, using defaults"); // failed to load any providers, initialize with our defaults addProvider(new Provider(Provider.Type.STORE, - "imap", "com.sun.mail.imap.IMAPStore", + "imap", "org.eclipse.angus.mail.imap.IMAPStore", "Oracle", Version.version)); addProvider(new Provider(Provider.Type.STORE, - "imaps", "com.sun.mail.imap.IMAPSSLStore", + "imaps", "org.eclipse.angus.mail.imap.IMAPSSLStore", "Oracle", Version.version)); addProvider(new Provider(Provider.Type.STORE, - "pop3", "com.sun.mail.pop3.POP3Store", + "pop3", "org.eclipse.angus.mail.pop3.POP3Store", "Oracle", Version.version)); addProvider(new Provider(Provider.Type.STORE, - "pop3s", "com.sun.mail.pop3.POP3SSLStore", + "pop3s", "org.eclipse.angus.mail.pop3.POP3SSLStore", "Oracle", Version.version)); addProvider(new Provider(Provider.Type.TRANSPORT, - "smtp", "com.sun.mail.smtp.SMTPTransport", + "smtp", "org.eclipse.angus.mail.smtp.SMTPTransport", "Oracle", Version.version)); addProvider(new Provider(Provider.Type.TRANSPORT, - "smtps", "com.sun.mail.smtp.SMTPSSLTransport", + "smtps", "org.eclipse.angus.mail.smtp.SMTPSSLTransport", "Oracle", Version.version)); } @@ -1307,6 +1313,46 @@ public ClassLoader run() { ); } + private static ClassLoader[] getClassLoaders(final Class... classes) { + return AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public ClassLoader[] run() { + ClassLoader[] loaders = new ClassLoader[classes.length]; + int w = 0; + for (Class k : classes) { + ClassLoader cl = null; + if (k == Thread.class) { + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { + } + } else if (k == System.class) { + try { + cl = ClassLoader.getSystemClassLoader(); + } catch (SecurityException ex) { + } + } else { + try { + cl = k.getClassLoader(); + } catch (SecurityException ex) { + } + } + + if (cl != null) { + loaders[w++] = cl; + } + } + + if (loaders.length != w) { + loaders = Arrays.copyOf(loaders, w); + } + return loaders; + } + } + ); + } + private static InputStream getResourceAsStream(final Class c, final String name) throws IOException { try { return AccessController.doPrivileged( @@ -1386,33 +1432,44 @@ EventQueue getEventQueue() { return q; } - private static final String OSGI_SERVICE_LOADER_CLASS_NAME = "org.glassfish.hk2.osgiresourcelocator.ServiceLoader"; + private static Class[] getHk2ServiceLoaderTargets(Class factoryClass) { + ClassLoader[] loaders = getClassLoaders(Thread.class, factoryClass, System.class); - private static boolean isHk2Available() { - try { - Class.forName(OSGI_SERVICE_LOADER_CLASS_NAME); - return true; - } catch (ClassNotFoundException ignored) { + Class[] classes = new Class[loaders.length]; + int w = 0; + for (ClassLoader loader : loaders) { + if (loader != null) { + try { + classes[w++] = Class.forName("org.glassfish.hk2.osgiresourcelocator.ServiceLoader", false, loader); + } catch (Exception | LinkageError ignored) { + } //GlassFish class loaders can throw undocumented exceptions + } } - return false; + + if (classes.length != w) { + classes = Arrays.copyOf(classes, w); + } + return classes; } @SuppressWarnings({"unchecked"}) - private Iterator lookupUsingHk2ServiceLoader(String factoryId) { - try { - // Use reflection to avoid having any dependency on HK2 ServiceLoader class - Class serviceClass = Class.forName(factoryId); - Class[] args = new Class[]{serviceClass}; - Class target = Class.forName(OSGI_SERVICE_LOADER_CLASS_NAME); - Method m = target.getMethod("lookupProviderInstances", Class.class); - Iterable result = ((Iterable) m.invoke(null, (Object[]) args)); - return result != null ? result.iterator() : Collections.emptyIterator(); - } catch (Exception ignored) { - // log and continue - return Collections.emptyIterator(); + private Iterator lookupUsingHk2ServiceLoader(Class factoryId, ClassLoader loader) { + for (Class target : getHk2ServiceLoaderTargets(factoryId)) { + try { + // Use reflection to avoid having any dependency on HK2 ServiceLoader class + Class serviceClass = Class.forName(factoryId.getName(), false, loader); + Class[] args = new Class[]{serviceClass}; + Method m = target.getMethod("lookupProviderInstances", Class.class); + Iterable result = ((Iterable) m.invoke(null, (Object[]) args)); + if (result != null) { + return result.iterator(); + } + } catch (Exception ignored) { + // log and continue + } } + return Collections.emptyIterator(); } - } /** diff --git a/api/src/main/java/jakarta/mail/util/FactoryFinder.java b/api/src/main/java/jakarta/mail/util/FactoryFinder.java index e1f91b3e..9c144941 100644 --- a/api/src/main/java/jakarta/mail/util/FactoryFinder.java +++ b/api/src/main/java/jakarta/mail/util/FactoryFinder.java @@ -17,6 +17,7 @@ package jakarta.mail.util; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Iterator; import java.util.ServiceLoader; @@ -33,46 +34,70 @@ class FactoryFinder { * @throws RuntimeException if there is an error */ static T find(Class factoryClass) throws RuntimeException { + T result; + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + if (loader != null) { + result = find(factoryClass, loader); + if (result != null) { + return result; + } + } - String factoryId = factoryClass.getName(); + //JakartaMail API ClassLoader / caller classloader + loader = factoryClass.getClassLoader(); + if (loader != null) { + result = find(factoryClass, loader); + if (result != null) { + return result; + } + } + //Fallback to system class loader + result = find(factoryClass, ClassLoader.getSystemClassLoader()); + if (result != null) { + return result; + } + + throw new IllegalStateException("No provider of " + factoryClass.getName() + " was found"); + } + + private static T find(Class factoryClass, ClassLoader loader) throws RuntimeException { // Use the system property first - String className = fromSystemProperty(factoryId); + String className = fromSystemProperty(factoryClass.getName()); if (className != null) { - T result = newInstance(className); + T result = newInstance(className, factoryClass, loader); if (result != null) { return result; } } // standard services: java.util.ServiceLoader - T factory = factoryFromServiceLoader(factoryClass); + T factory = factoryFromServiceLoader(factoryClass, loader); if (factory != null) { return factory; } // handling Glassfish/OSGi (platform specific default) - if (isHk2Available()) { - T result = lookupUsingHk2ServiceLoader(factoryId); - if (result != null) { - return result; - } + + T result = lookupUsingHk2ServiceLoader(factoryClass, loader); + if (result != null) { + return result; } - throw new IllegalStateException("Not provider of " + factoryClass.getName() + " was found"); + + return null; } - @SuppressWarnings({"unchecked"}) - private static T newInstance(String className) throws RuntimeException { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + private static T newInstance(String className, Class factoryClass, ClassLoader classLoader) throws RuntimeException { checkPackageAccess(className); - Class clazz = null; try { - if (classLoader == null) { - clazz = (Class) Class.forName(className); - } else { - clazz = (Class) classLoader.loadClass(className); + Class clazz; + if (classLoader == null) { //Match behavior of ServiceLoader + classLoader = ClassLoader.getSystemClassLoader(); } - return clazz.getConstructor().newInstance(); + clazz = Class.forName(className, false, classLoader); + return clazz.asSubclass(factoryClass).getConstructor().newInstance(); + } catch (ClassCastException wrongLoader) { + return null; } catch (ReflectiveOperationException e) { throw new IllegalArgumentException("Cannot instance " + className, e); } @@ -82,47 +107,64 @@ private static String fromSystemProperty(String factoryId) { String systemProp = System.getProperty(factoryId); return systemProp; } - - private static final String OSGI_SERVICE_LOADER_CLASS_NAME = "org.glassfish.hk2.osgiresourcelocator.ServiceLoader"; - - private static boolean isHk2Available() { - try { - Class.forName(OSGI_SERVICE_LOADER_CLASS_NAME); - return true; - } catch (ClassNotFoundException ignored) { + + private static Class[] getHk2ServiceLoaderTargets(Class factoryClass) { + ClassLoader[] loaders = new ClassLoader[]{ + Thread.currentThread().getContextClassLoader(), + factoryClass.getClassLoader(), + ClassLoader.getSystemClassLoader()}; + + Class[] classes = new Class[loaders.length]; + int w = 0; + for (ClassLoader loader : loaders) { + if (loader != null) { + try { + classes[w++] = Class.forName("org.glassfish.hk2.osgiresourcelocator.ServiceLoader", false, loader); + } catch (Exception | LinkageError ignored) { + } //GlassFish class loaders can throw undocumented exceptions + } } - return false; + + if (classes.length != w) { + classes = Arrays.copyOf(classes, w); + } + return classes; } @SuppressWarnings({"unchecked"}) - private static T lookupUsingHk2ServiceLoader(String factoryId) { - try { - // Use reflection to avoid having any dependency on HK2 ServiceLoader class - Class serviceClass = Class.forName(factoryId); - Class[] args = new Class[]{serviceClass}; - Class target = Class.forName(OSGI_SERVICE_LOADER_CLASS_NAME); - Method m = target.getMethod("lookupProviderInstances", Class.class); - Iterable iterable = ((Iterable) m.invoke(null, (Object[]) args)); - if (iterable == null) { - return null; + private static T lookupUsingHk2ServiceLoader(Class factoryClass, ClassLoader loader) { + for (Class target : getHk2ServiceLoaderTargets(factoryClass)) { + try { + // Use reflection to avoid having any dependency on HK2 ServiceLoader class + Class serviceClass = Class.forName(factoryClass.getName(), false, loader); + Class[] args = new Class[]{serviceClass}; + Method m = target.getMethod("lookupProviderInstances", Class.class); + Iterable iterable = ((Iterable) m.invoke(null, (Object[]) args)); + if (iterable != null) { + Iterator iter = iterable.iterator(); + if (iter.hasNext()) { + return factoryClass.cast(iter.next()); //Verify classloader. + } + } + } catch (Exception ignored) { + // log and continue } - Iterator iter = iterable.iterator(); - return iter.hasNext() ? (T) iter.next() : null; - } catch (Exception ignored) { - // log and continue - return null; } + return null; } - private static T factoryFromServiceLoader(Class factory) { + private static T factoryFromServiceLoader(Class factory, ClassLoader loader) { + //ClassLoader of null is treated as system classloader try { - ServiceLoader sl = ServiceLoader.load(factory); + ServiceLoader sl = ServiceLoader.load(factory, loader); Iterator iter = sl.iterator(); if (iter.hasNext()) { - return iter.next(); + return factory.cast(iter.next()); //Verify loader } else { return null; } + } catch (ClassCastException wrongLoader) { + return null; } catch (Throwable t) { // For example, ServiceConfigurationError can be thrown if the factory class is not declared with 'uses' in module-info throw new IllegalStateException("Cannot load " + factory + " as ServiceLoader", t); diff --git a/api/src/test/java/jakarta/mail/util/DummyStreamProvider.java b/api/src/test/java/jakarta/mail/util/DummyStreamProvider.java index b3e20a9e..e13550e7 100644 --- a/api/src/test/java/jakarta/mail/util/DummyStreamProvider.java +++ b/api/src/test/java/jakarta/mail/util/DummyStreamProvider.java @@ -21,6 +21,9 @@ import java.io.OutputStream; public class DummyStreamProvider implements StreamProvider { + + public DummyStreamProvider() { + } @Override public InputStream inputBase64(InputStream in) { diff --git a/api/src/test/java/jakarta/mail/util/FactoryFinderTest.java b/api/src/test/java/jakarta/mail/util/FactoryFinderTest.java index 53ffd1f7..6a3ec317 100644 --- a/api/src/test/java/jakarta/mail/util/FactoryFinderTest.java +++ b/api/src/test/java/jakarta/mail/util/FactoryFinderTest.java @@ -24,6 +24,9 @@ public class FactoryFinderTest { + public FactoryFinderTest() { + } + @Test public void specifiedInSystemProperty() { System.setProperty(Class1.class.getName(), Class2.class.getName()); @@ -55,12 +58,30 @@ public void doesNotExist() { // } } + + @Test + public void contextClassLoaderIsBootLoader() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader((ClassLoader) null); + //Thread.currentThread().setContextClassLoader(ClassLoader.getPlatformClassLoader()); + try { + doesNotExist(); + specifiedInSystemProperty(); + specifiedInServiceLoader(); + } finally { + Thread.currentThread().setContextClassLoader(cl); + } + } + public static class Class1 { + public Class1() {} } public static class Class2 extends Class1 { + public Class2() {} } public static class Class3 { + public Class3() {} } } diff --git a/doc/release/CHANGES.txt b/doc/release/CHANGES.txt index 4e26a970..214ba6c2 100644 --- a/doc/release/CHANGES.txt +++ b/doc/release/CHANGES.txt @@ -19,9 +19,10 @@ Bug IDs that start with "G" can be found in the GlassFish Issue Tracker Seven digit bug numbers are from the old Sun bug database, which is no longer available. - CHANGES IN THE 2.1.3 RELEASE - ---------------------------- + CHANGES IN THE 2.1.3 RELEASE + ---------------------------- E 695 SharedFileInputStream should comply with spec +E 665 Jakarta Mail erroneously assumes that classes can be loaded from Thread#getContextClassLoader CHANGES IN THE 2.1.2 RELEASE