From e4f301e2f361819fd5ab8415affddac8cff494c9 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sat, 12 Sep 2026 18:13:28 +0200 Subject: [PATCH 1/3] Service discovery no init --- api/src/main/java/javax/jdo/JDOHelper.java | 18 ++++- .../resources/javax/jdo/Bundle.properties | 3 + .../jdo/JDOHelperServiceValidationTest.java | 80 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java diff --git a/api/src/main/java/javax/jdo/JDOHelper.java b/api/src/main/java/javax/jdo/JDOHelper.java index aa1d0601f..fb5e093d7 100644 --- a/api/src/main/java/javax/jdo/JDOHelper.java +++ b/api/src/main/java/javax/jdo/JDOHelper.java @@ -971,12 +971,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 +1597,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..457e4d45c --- /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"); + } +} From 4e80dd7f08c3a9a49f9324bc263a18ca2126cf42 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 13 Sep 2026 14:13:50 +0200 Subject: [PATCH 2/3] Fix formatting --- .../test/java/javax/jdo/JDOHelperServiceValidationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java b/api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java index 457e4d45c..0e273084b 100644 --- a/api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java +++ b/api/src/test/java/javax/jdo/JDOHelperServiceValidationTest.java @@ -29,8 +29,8 @@ 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 + * 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 { From caf48f427aa3821edf84cbba1df105b832a94147 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Tue, 6 Oct 2026 21:48:04 +0200 Subject: [PATCH 3/3] Move regex outside loop --- api/src/main/java/javax/jdo/JDOHelper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/javax/jdo/JDOHelper.java b/api/src/main/java/javax/jdo/JDOHelper.java index fb5e093d7..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) {