From 751f97ceaa419d5a6e02c848240fbb04cf4785e1 Mon Sep 17 00:00:00 2001 From: pmeloni Date: Fri, 5 Jun 2026 12:34:12 +0200 Subject: [PATCH] Resolves RHEL-183992: remove tomcat clustering JAR from RPM builds --- rhel-168577.patch | 346 ++++++++++++++++++++++++++++++++++++++++++++++ tomcat.spec | 19 ++- 2 files changed, 360 insertions(+), 5 deletions(-) create mode 100644 rhel-168577.patch diff --git a/rhel-168577.patch b/rhel-168577.patch new file mode 100644 index 0000000..5d196da --- /dev/null +++ b/rhel-168577.patch @@ -0,0 +1,346 @@ +diff --git a/java/org/apache/catalina/storeconfig/LocalStrings.properties b/java/org/apache/catalina/storeconfig/LocalStrings.properties +index d3e8585df5..4c2aa53bee 100644 +--- a/java/org/apache/catalina/storeconfig/LocalStrings.properties ++++ b/java/org/apache/catalina/storeconfig/LocalStrings.properties +@@ -28,8 +28,11 @@ factory.storeTag=store tag [{0}] ( Object: [{1}] ) + globalNamingResourcesSF.noFactory=Cannot find NamingResources store factory + globalNamingResourcesSF.wrongElement=Wrong element [{0}] + ++registry.interfacesLoaded=Loaded [{0}] interface classes for registry + registry.loadClassFailed=Failed to load class [{0}] + registry.noDescriptor=Can't find descriptor for key [{0}] ++registry.optionalClassLoaded=Loaded optional class [{0}] ++registry.optionalClassNotFound=Optional class [{0}] not found, skipping + + standardContextSF.cannotWriteFile=Cannot write file at [{0}] + standardContextSF.canonicalPathError=Failed to obtain the canonical path of the configuration file [{0}] +diff --git a/java/org/apache/catalina/storeconfig/StandardEngineSF.java b/java/org/apache/catalina/storeconfig/StandardEngineSF.java +index 0f600ee5c1..b6bd5d7168 100644 +--- a/java/org/apache/catalina/storeconfig/StandardEngineSF.java ++++ b/java/org/apache/catalina/storeconfig/StandardEngineSF.java +@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener; + import org.apache.catalina.Realm; + import org.apache.catalina.Valve; + import org.apache.catalina.core.StandardEngine; +-import org.apache.catalina.ha.ClusterValve; + + /** + * Store server.xml Element Engine + */ + public class StandardEngineSF extends StoreFactoryBase { + ++ private static final Class clusterValveClass; ++ static { ++ Class clazz = null; ++ try { ++ clazz = Class.forName("org.apache.catalina.ha.ClusterValve"); ++ } catch (ClassNotFoundException e) { ++ // Expected when clustering JARs are not present ++ } ++ clusterValveClass = clazz; ++ } ++ + /** + * Store the specified Engine properties. + *

+@@ -64,7 +74,7 @@ public class StandardEngineSF extends StoreFactoryBase { + if (valves != null && valves.length > 0) { + List engineValves = new ArrayList<>(); + for (Valve valve : valves) { +- if (!(valve instanceof ClusterValve)) { ++ if (clusterValveClass == null || !clusterValveClass.isInstance(valve)) { + engineValves.add(valve); + } + } +diff --git a/java/org/apache/catalina/storeconfig/StandardHostSF.java b/java/org/apache/catalina/storeconfig/StandardHostSF.java +index 60d473982b..1e8f2d3a58 100644 +--- a/java/org/apache/catalina/storeconfig/StandardHostSF.java ++++ b/java/org/apache/catalina/storeconfig/StandardHostSF.java +@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener; + import org.apache.catalina.Realm; + import org.apache.catalina.Valve; + import org.apache.catalina.core.StandardHost; +-import org.apache.catalina.ha.ClusterValve; + + /** + * Store server.xml Element Host + */ + public class StandardHostSF extends StoreFactoryBase { + ++ private static final Class clusterValveClass; ++ static { ++ Class clazz = null; ++ try { ++ clazz = Class.forName("org.apache.catalina.ha.ClusterValve"); ++ } catch (ClassNotFoundException e) { ++ // Expected when clustering JARs are not present ++ } ++ clusterValveClass = clazz; ++ } ++ + /** + * Store the specified Host properties and children (Listener,Alias,Realm,Valve,Cluster, Context) + *

+@@ -68,7 +78,7 @@ public class StandardHostSF extends StoreFactoryBase { + if (valves != null && valves.length > 0) { + List hostValves = new ArrayList<>(); + for (Valve valve : valves) { +- if (!(valve instanceof ClusterValve)) { ++ if (clusterValveClass == null || !clusterValveClass.isInstance(valve)) { + hostValves.add(valve); + } + } +diff --git a/java/org/apache/catalina/storeconfig/StoreRegistry.java b/java/org/apache/catalina/storeconfig/StoreRegistry.java +index c17dd3817e..ee803abe36 100644 +--- a/java/org/apache/catalina/storeconfig/StoreRegistry.java ++++ b/java/org/apache/catalina/storeconfig/StoreRegistry.java +@@ -16,7 +16,9 @@ + */ + package org.apache.catalina.storeconfig; + ++import java.util.ArrayList; + import java.util.HashMap; ++import java.util.List; + import java.util.Map; + + import javax.naming.directory.DirContext; +@@ -28,17 +30,6 @@ import org.apache.catalina.Realm; + import org.apache.catalina.Valve; + import org.apache.catalina.WebResourceRoot; + import org.apache.catalina.WebResourceSet; +-import org.apache.catalina.ha.CatalinaCluster; +-import org.apache.catalina.ha.ClusterDeployer; +-import org.apache.catalina.ha.ClusterListener; +-import org.apache.catalina.tribes.Channel; +-import org.apache.catalina.tribes.ChannelInterceptor; +-import org.apache.catalina.tribes.ChannelReceiver; +-import org.apache.catalina.tribes.ChannelSender; +-import org.apache.catalina.tribes.Member; +-import org.apache.catalina.tribes.MembershipService; +-import org.apache.catalina.tribes.MessageListener; +-import org.apache.catalina.tribes.transport.DataSender; + import org.apache.coyote.UpgradeProtocol; + import org.apache.juli.logging.Log; + import org.apache.juli.logging.LogFactory; +@@ -61,11 +52,74 @@ public class StoreRegistry { + private String version; + + // Access Information +- private static final Class[] interfaces = { CatalinaCluster.class, ChannelSender.class, ChannelReceiver.class, +- Channel.class, MembershipService.class, ClusterDeployer.class, Realm.class, Manager.class, DirContext.class, +- LifecycleListener.class, Valve.class, ClusterListener.class, MessageListener.class, DataSender.class, +- ChannelInterceptor.class, Member.class, WebResourceRoot.class, WebResourceSet.class, +- CredentialHandler.class, UpgradeProtocol.class, CookieProcessor.class }; ++ // Lazily initialized to gracefully handle optional features like clustering ++ private static volatile Class[] interfaces = null; ++ ++ /** ++ * Initialize the interfaces array with all available classes. ++ * Uses dynamic loading for optional classes (e.g., clustering) to avoid ++ * ClassNotFoundException when those JARs are not present. This approach ++ * is consistent with how Catalina.addClusterRuleSet() handles clustering. ++ */ ++ private static Class[] getInterfaces() { ++ if (interfaces == null) { ++ synchronized (StoreRegistry.class) { ++ if (interfaces == null) { ++ // Required interfaces - always present ++ List> list = new ArrayList<>(); ++ list.add(Realm.class); ++ list.add(Manager.class); ++ list.add(DirContext.class); ++ list.add(LifecycleListener.class); ++ list.add(Valve.class); ++ list.add(WebResourceRoot.class); ++ list.add(WebResourceSet.class); ++ list.add(CredentialHandler.class); ++ list.add(UpgradeProtocol.class); ++ list.add(CookieProcessor.class); ++ ++ // Optional clustering interfaces - load dynamically to support ++ // deployments where clustering JARs may not be present ++ tryAddClass(list, "org.apache.catalina.ha.CatalinaCluster"); ++ tryAddClass(list, "org.apache.catalina.tribes.ChannelSender"); ++ tryAddClass(list, "org.apache.catalina.tribes.ChannelReceiver"); ++ tryAddClass(list, "org.apache.catalina.tribes.Channel"); ++ tryAddClass(list, "org.apache.catalina.tribes.MembershipService"); ++ tryAddClass(list, "org.apache.catalina.ha.ClusterDeployer"); ++ tryAddClass(list, "org.apache.catalina.ha.ClusterListener"); ++ tryAddClass(list, "org.apache.catalina.tribes.MessageListener"); ++ tryAddClass(list, "org.apache.catalina.tribes.transport.DataSender"); ++ tryAddClass(list, "org.apache.catalina.tribes.ChannelInterceptor"); ++ tryAddClass(list, "org.apache.catalina.tribes.Member"); ++ ++ interfaces = list.toArray(new Class[0]); ++ ++ if (log.isDebugEnabled()) { ++ log.debug(sm.getString("registry.interfacesLoaded", Integer.valueOf(interfaces.length))); ++ } ++ } ++ } ++ } ++ return interfaces; ++ } ++ ++ /** ++ * Try to load a class by name and add it to the list if successful. ++ * Logs at TRACE level if the class is not available. ++ */ ++ private static void tryAddClass(List> list, String className) { ++ try { ++ Class clazz = Class.forName(className, false, StoreRegistry.class.getClassLoader()); ++ list.add(clazz); ++ if (log.isTraceEnabled()) { ++ log.trace(sm.getString("registry.optionalClassLoaded", className)); ++ } ++ } catch (ClassNotFoundException | NoClassDefFoundError e) { ++ if (log.isTraceEnabled()) { ++ log.trace(sm.getString("registry.optionalClassNotFound", className)); ++ } ++ } ++ } + + /** + * @return the name +@@ -116,9 +170,10 @@ public class StoreRegistry { + } + if (aClass != null) { + desc = descriptors.get(aClass.getName()); +- for (int i = 0; desc == null && i < interfaces.length; i++) { +- if (interfaces[i].isAssignableFrom(aClass)) { +- desc = descriptors.get(interfaces[i].getName()); ++ Class[] availableInterfaces = getInterfaces(); ++ for (int i = 0; desc == null && i < availableInterfaces.length; i++) { ++ if (availableInterfaces[i].isAssignableFrom(aClass)) { ++ desc = descriptors.get(availableInterfaces[i].getName()); + } + } + } +diff --git a/test/org/apache/catalina/storeconfig/TestStoreRegistry.java b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java +new file mode 100644 +index 0000000000..e6869d3642 +--- /dev/null ++++ b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java +@@ -0,0 +1,104 @@ ++/* ++ * 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 ++ * ++ * http://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 org.apache.catalina.storeconfig; ++ ++import java.lang.reflect.Method; ++ ++import org.junit.Assert; ++import org.junit.Test; ++ ++import org.apache.catalina.Manager; ++import org.apache.catalina.Realm; ++import org.apache.catalina.Valve; ++ ++/** ++ * Test StoreRegistry behavior, particularly dynamic loading of optional classes like clustering. ++ * ++ * Verifies StoreRegistry uses the same dynamic loading pattern. ++ */ ++public class TestStoreRegistry { ++ ++ /** ++ * Test that clustering classes are dynamically loaded like other Tomcat components. ++ * ++ * StoreRegistry should initialize successfully whether clustering is available or not. ++ * This matches the pattern used in Catalina.addClusterRuleSet(). ++ */ ++ @Test ++ public void testClusteringClassesOptional() throws Exception { ++ // Verify StoreRegistry initializes successfully with dynamic class loading ++ StoreRegistry registry = new StoreRegistry(); ++ Assert.assertNotNull("Registry should initialize with dynamic loading", registry); ++ ++ // Trigger lazy loading of interfaces array ++ Method getInterfacesMethod = StoreRegistry.class.getDeclaredMethod("getInterfaces"); ++ getInterfacesMethod.setAccessible(true); ++ ++ Class[] interfaces = (Class[]) getInterfacesMethod.invoke(null); ++ Assert.assertNotNull("Interfaces should load dynamically", interfaces); ++ ++ // Test passes if we get here without ClassNotFoundException. ++ // The actual number of interfaces loaded depends on whether clustering is available, ++ // but we should always have at least the core 10 interfaces. ++ Assert.assertTrue("Should have at least 10 core interfaces", ++ interfaces.length >= 10); ++ ++ // Verify required core interfaces are always present ++ boolean hasRealm = false; ++ boolean hasManager = false; ++ boolean hasValve = false; ++ ++ for (Class iface : interfaces) { ++ if (iface.equals(Realm.class)) { ++ hasRealm = true; ++ } ++ if (iface.equals(Manager.class)) { ++ hasManager = true; ++ } ++ if (iface.equals(Valve.class)) { ++ hasValve = true; ++ } ++ } ++ ++ Assert.assertTrue("Should contain Realm interface", hasRealm); ++ Assert.assertTrue("Should contain Manager interface", hasManager); ++ Assert.assertTrue("Should contain Valve interface", hasValve); ++ } ++ ++ /** ++ * Test that findDescription works with interface inheritance and ++ * dynamically loaded interfaces. ++ */ ++ @Test ++ public void testFindDescriptionWithDynamicInterfaces() throws Exception { ++ StoreRegistry registry = new StoreRegistry(); ++ ++ // Register a description for the Valve interface ++ StoreDescription valveDesc = new StoreDescription(); ++ valveDesc.setId(Valve.class.getName()); ++ valveDesc.setTag("Valve"); ++ valveDesc.setTagClass(Valve.class.getName()); ++ registry.registerDescription(valveDesc); ++ ++ // AccessLogValve implements Valve interface - should find via dynamic interface matching ++ String accessLogValveClass = "org.apache.catalina.valves.AccessLogValve"; ++ StoreDescription foundDesc = registry.findDescription(accessLogValveClass); ++ ++ Assert.assertNotNull("Should find description via interface matching", foundDesc); ++ Assert.assertEquals("Should match Valve descriptor", "Valve", foundDesc.getTag()); ++ } ++} +diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml +index 808165f044..792dad2fa4 100644 +--- a/webapps/docs/changelog.xml ++++ b/webapps/docs/changelog.xml +@@ -113,6 +113,13 @@ + in RFC 9110 that hash functions used to generate strong ETags should be + collision resistant. (markt) + ++ ++ Update StoreRegistry to dynamically load optional clustering ++ classes rather than statically referencing them. This matches the pattern ++ used in Catalina.addClusterRuleSet() and prevents ++ NoClassDefFoundError when StoreConfigLifecycleListener ++ is configured but clustering classes are not available. (csutherl) ++ + + Correct a regression in the fix for 69781 that broke + FileStore. (markt) diff --git a/tomcat.spec b/tomcat.spec index 0252713..48cc1df 100644 --- a/tomcat.spec +++ b/tomcat.spec @@ -56,7 +56,7 @@ Name: tomcat Epoch: 1 Version: %{major_version}.%{minor_version}.%{micro_version} -Release: 1%{?dist} +Release: 2%{?dist} Summary: Apache Servlet/JSP Engine, RI for Servlet %{servletspec}/JSP %{jspspec} API License: ASL 2.0 @@ -81,8 +81,10 @@ Patch2: %{name}-build.patch Patch3: %{name}-%{major_version}.%{minor_version}-catalina-policy.patch Patch4: rhbz-1857043.patch Patch6: %{name}-%{major_version}.%{minor_version}-bnd-annotation.patch +Patch7: rhel-168577.patch BuildArch: noarch +ExclusiveArch: %{java_arches} noarch BuildRequires: ant BuildRequires: ecj >= 1:4.10 @@ -197,6 +199,7 @@ find . -type f \( -name "*.bat" -o -name "*.class" -o -name Thumbs.db -o -name " %patch -P3 -p0 %patch -P4 -p0 %patch -P6 -p0 +%patch -P7 -p1 # Remove webservices naming resources as it's generally unused %{__rm} -rf java/org/apache/naming/factory/webservices @@ -208,6 +211,8 @@ find . -type f \( -name "*.bat" -o -name "*.class" -o -name Thumbs.db -o -name " %mvn_alias "org.apache.tomcat:tomcat-jsp-api" "org.eclipse.jetty.orbit:javax.servlet.jsp" %mvn_package ":tomcat-servlet-api" tomcat-servlet-api +%pom_remove_dep org.apache.tomcat:tomcat-tribes res/maven/tomcat-storeconfig.pom +%pom_remove_dep org.apache.tomcat:tomcat-catalina-ha res/maven/tomcat-storeconfig.pom %build export OPT_JAR_LIST="xalan-j2-serializer" @@ -279,6 +284,10 @@ pushd output/build %{__cp} -a webapps/* ${RPM_BUILD_ROOT}%{appdir} popd +# Clustering is unsupported in RHEL +rm -f ${RPM_BUILD_ROOT}%{libdir}/catalina-ha.jar +rm -f ${RPM_BUILD_ROOT}%{libdir}/catalina-tribes.jar + %{__sed} -e "s|\@\@\@TCHOME\@\@\@|%{homedir}|g" \ -e "s|\@\@\@TCTEMP\@\@\@|%{tempdir}|g" \ -e "s|\@\@\@LIBDIR\@\@\@|%{_libdir}|g" %{SOURCE1} \ @@ -372,8 +381,6 @@ popd %mvn_artifact res/maven/tomcat-api.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-api.jar %mvn_file org.apache.tomcat:tomcat-catalina-ant tomcat/catalina-ant %mvn_artifact res/maven/tomcat-catalina-ant.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-ant.jar -%mvn_file org.apache.tomcat:tomcat-catalina-ha tomcat/catalina-ha -%mvn_artifact res/maven/tomcat-catalina-ha.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-ha.jar %mvn_file org.apache.tomcat:tomcat-catalina tomcat/catalina %mvn_artifact res/maven/tomcat-catalina.pom ${RPM_BUILD_ROOT}%{libdir}/catalina.jar %mvn_artifact res/maven/tomcat-coyote.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-coyote.jar @@ -400,8 +407,6 @@ popd %mvn_artifact res/maven/tomcat-ssi.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-ssi.jar %mvn_file org.apache.tomcat:tomcat-storeconfig tomcat/catalina-storeconfig %mvn_artifact res/maven/tomcat-storeconfig.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-storeconfig.jar -%mvn_file org.apache.tomcat:tomcat-tribes tomcat/catalina-tribes -%mvn_artifact res/maven/tomcat-tribes.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-tribes.jar %mvn_artifact res/maven/tomcat-util-scan.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-util-scan.jar %mvn_artifact res/maven/tomcat-util.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-util.jar %mvn_file org.apache.tomcat:tomcat-websocket-api tomcat/websocket-api @@ -562,6 +567,10 @@ fi %{appdir}/ROOT %changelog +* Tue Jun 4 2026 Pietro Meloni - 1:9.0.117-2 +- Resolves: RHEL-183992 Remove tomcat clustering JAR from RPM builds +- Exclude i686 architecture from build + * Wed May 26 2026 Pietro Meloni - 1:9.0.117-1 - Resolves: RHEL-150714 Certificate revocation bypass due to improper OCSP response validation - Resolves: