diff --git a/api/src/main/java/javax/jdo/JDOHelper.java b/api/src/main/java/javax/jdo/JDOHelper.java index aa1d0601f..2c62ad3f6 100644 --- a/api/src/main/java/javax/jdo/JDOHelper.java +++ b/api/src/main/java/javax/jdo/JDOHelper.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.regex.Pattern; import javax.jdo.spi.I18NHelper; import javax.jdo.spi.JDOImplHelper; import javax.jdo.spi.JDOImplHelper.StateInterrogationBooleanReturn; @@ -741,6 +742,7 @@ protected static String getClassNameFromURL(URL url) throws IOException { InputStream is = openStream(url); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line = null; + Pattern splitRegex = Pattern.compile("\\s"); try { while ((line = reader.readLine()) != null) { line = line.trim(); @@ -748,7 +750,7 @@ protected static String getClassNameFromURL(URL url) throws IOException { continue; } // else assume first line of text is the PMF class name - String[] tokens = line.split("\\s"); + String[] tokens = splitRegex.split(line); String pmfClassName = tokens[0]; int indexOfComment = pmfClassName.indexOf("#"); if (indexOfComment == -1) { @@ -971,12 +973,20 @@ public static PersistenceManagerFactory getPersistenceManagerFactory( protected static PersistenceManagerFactory invokeGetPersistenceManagerFactoryOnImplementation( String pmfClassName, Map overrides, Map properties, ClassLoader cl) { try { - Class implClass = forName(pmfClassName, true, cl); + // load without initializing: no code of the candidate class (which may be named by + // the first-match-wins services lookup) runs before the method has been verified + Class implClass = forName(pmfClassName, false, cl); Method m = getMethod( implClass, "getPersistenceManagerFactory", // NOI18N overrides != null ? new Class[] {Map.class, Map.class} : new Class[] {Map.class}); + if (!PersistenceManagerFactory.class.isAssignableFrom(m.getReturnType())) { + // reject before invoking: otherwise the static initializers and method body of an + // arbitrary candidate class would run before the type check + throw new JDOFatalInternalException( + MSG.msg("EXC_GetPMFClassCastException", pmfClassName)); // NOI18N + } PersistenceManagerFactory pmf = (PersistenceManagerFactory) invoke( @@ -1589,7 +1599,13 @@ public static JDOEnhancer getEnhancer(ClassLoader loader) { numberOfJDOEnhancers++; try { String enhancerClassName = getClassNameFromURL(urls.nextElement()); - Class enhancerClass = forName(enhancerClassName, true, ctrLoader); + // load without initializing and verify assignability before running any code of + // the candidate class named by the (first-match-wins) services file + Class enhancerClass = forName(enhancerClassName, false, ctrLoader); + if (!JDOEnhancer.class.isAssignableFrom(enhancerClass)) { + throw new JDOFatalUserException( + MSG.msg("EXC_GetEnhancerClassNotAssignable", enhancerClassName)); // NOI18N + } return (JDOEnhancer) enhancerClass.getDeclaredConstructor().newInstance(); } catch (Exception ex) { // remember exceptions from failed enhancer invocations diff --git a/api/src/main/resources/javax/jdo/Bundle.properties b/api/src/main/resources/javax/jdo/Bundle.properties index 208ef4561..4d3dd8289 100644 --- a/api/src/main/resources/javax/jdo/Bundle.properties +++ b/api/src/main/resources/javax/jdo/Bundle.properties @@ -125,6 +125,9 @@ EXC_GetEnhancerNoValidEnhancerAvailable=\ There are {0} services entries for the JDOEnhancer; \ there were no valid JDOEnhancer implementations found in the CLASSPATH. \ The file META-INF/services/javax.jdo.JDOEnhancer should name the implementation class. +EXC_GetEnhancerClassNotAssignable=The class "{0}" named in a \ +META-INF/services/javax.jdo.JDOEnhancer resource does not implement \ +javax.jdo.JDOEnhancer. The class was not instantiated. MSG_EnhancerUsage=\ Usage: java -cp javax.jdo.Enhancer \n\ options:\n\ diff --git a/api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java b/api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java new file mode 100644 index 000000000..0e273084b --- /dev/null +++ b/api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.jdo; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; +import java.util.Enumeration; +import javax.jdo.util.AbstractTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests that service-file discovery verifies the candidate class before running any of its code: a + * class named by a META-INF/services/javax.jdo.JDOEnhancer resource that does not implement + * JDOEnhancer must be rejected without executing its static initializer or constructor. + */ +class JDOHelperServiceValidationTest extends AbstractTest { + + /** Set by NotAnEnhancer's static and instance initializers; must remain false. */ + static volatile boolean notAnEnhancerCodeRan = false; + + /** A services-file candidate that is not a JDOEnhancer. */ + public static class NotAnEnhancer { + static { + notAnEnhancerCodeRan = true; + } + + public NotAnEnhancer() { + notAnEnhancerCodeRan = true; + } + } + + @Test + void testGetEnhancerRejectsNonEnhancerWithoutRunningItsCode() throws IOException { + File services = File.createTempFile("javax.jdo.JDOEnhancer", ".services"); + services.deleteOnExit(); + Files.write( + services.toPath(), (NotAnEnhancer.class.getName() + "\n").getBytes(StandardCharsets.UTF_8)); + final URL servicesURL = services.toURI().toURL(); + + // a loader whose only JDOEnhancer services entry names NotAnEnhancer + ClassLoader loader = + new ClassLoader(getClass().getClassLoader()) { + @Override + public Enumeration getResources(String name) throws IOException { + if ("META-INF/services/javax.jdo.JDOEnhancer".equals(name)) { + return Collections.enumeration(Collections.singletonList(servicesURL)); + } + return super.getResources(name); + } + }; + + Assertions.assertThrows( + JDOFatalUserException.class, + () -> JDOHelper.getEnhancer(loader), + "getEnhancer with only a non-enhancer candidate should fail"); + Assertions.assertFalse( + notAnEnhancerCodeRan, + "Code of the non-enhancer candidate must not run before the type check"); + } +}