This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit f74d9985e6f7c8eca9c1da3d14b777bd5ef7e4d3 Author: opencode <[email protected]> AuthorDate: Sun Oct 11 10:21:38 2026 +0200 Avoid NullPointerException when stopping channel components without a registry The channel components guarded their JMX deregistration at stop with their own ObjectName field, a hint set at start time, and then called JmxRegistry.getRegistry() unconditionally. The registry, however, is removed from the cache as soon as the channel MBean is deregistered externally, and getRegistry() then returns null once the JMX configuration no longer allows a new registry to be created, for example after the channel was renamed or JMX was disabled. The resulting NullPointerException aborted the stop of the channel, the interceptors, the transmitter and the receiver, or was swallowed by the membership services whose remaining cleanup, releasing the provider and the channel reference, was then skipped. The registry is now null-checked at all seven stop paths and only the deregistration itself is skipped when it is absent, as it is the only vehicle for unregistering. The rest of the stop always runs and the stale ObjectName is cleared. Add tests for the recreate-and-unregister path and for the invalidated configuration, the latter reproducing the NullPointerException on the previous code. --- .../tribes/group/ChannelInterceptorBase.java | 5 +- .../apache/catalina/tribes/group/GroupChannel.java | 5 +- .../catalina/tribes/membership/McastService.java | 5 +- .../tribes/membership/StaticMembershipService.java | 5 +- .../membership/cloud/CloudMembershipService.java | 5 +- .../catalina/tribes/transport/ReceiverBase.java | 5 +- .../tribes/transport/ReplicationTransmitter.java | 5 +- .../catalina/tribes/jmx/TestJmxRegistry.java | 103 +++++++++++++++++++++ 8 files changed, 131 insertions(+), 7 deletions(-) diff --git a/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java b/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java index 3e34f8f568..c535233675 100644 --- a/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java +++ b/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java @@ -188,7 +188,10 @@ public abstract class ChannelInterceptorBase implements ChannelInterceptor { getNext().stop(svc); } if (oname != null) { - JmxRegistry.getRegistry(channel).unregisterJmx(oname); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } oname = null; } channel = null; diff --git a/java/org/apache/catalina/tribes/group/GroupChannel.java b/java/org/apache/catalina/tribes/group/GroupChannel.java index b790ccf7c4..99eb3be4c9 100644 --- a/java/org/apache/catalina/tribes/group/GroupChannel.java +++ b/java/org/apache/catalina/tribes/group/GroupChannel.java @@ -503,7 +503,10 @@ public class GroupChannel extends ChannelInterceptorBase implements ManagedChann ownExecutor = false; } if (oname != null) { - JmxRegistry.getRegistry(this).unregisterJmx(oname); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(this); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } oname = null; } } diff --git a/java/org/apache/catalina/tribes/membership/McastService.java b/java/org/apache/catalina/tribes/membership/McastService.java index a368eae81f..cda9f12608 100644 --- a/java/org/apache/catalina/tribes/membership/McastService.java +++ b/java/org/apache/catalina/tribes/membership/McastService.java @@ -418,7 +418,10 @@ public class McastService extends MembershipServiceBase implements MessageListen try { if (impl != null && impl.stop(svc)) { if (oname != null) { - JmxRegistry.getRegistry(channel).unregisterJmx(oname); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } oname = null; } impl.setChannel(null); diff --git a/java/org/apache/catalina/tribes/membership/StaticMembershipService.java b/java/org/apache/catalina/tribes/membership/StaticMembershipService.java index 2eb1df86bf..74c0405eb0 100644 --- a/java/org/apache/catalina/tribes/membership/StaticMembershipService.java +++ b/java/org/apache/catalina/tribes/membership/StaticMembershipService.java @@ -102,7 +102,10 @@ public class StaticMembershipService extends MembershipServiceBase implements St try { if (provider != null && provider.stop(level)) { if (oname != null) { - JmxRegistry.getRegistry(channel).unregisterJmx(oname); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } oname = null; } provider = null; diff --git a/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java index 737503dd50..15d0fd45b0 100644 --- a/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java +++ b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java @@ -180,7 +180,10 @@ public class CloudMembershipService extends MembershipServiceBase implements Clo try { if (membershipProvider != null && membershipProvider.stop(level)) { if (oname != null) { - JmxRegistry.getRegistry(channel).unregisterJmx(oname); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } oname = null; } membershipProvider = null; diff --git a/java/org/apache/catalina/tribes/transport/ReceiverBase.java b/java/org/apache/catalina/tribes/transport/ReceiverBase.java index 85166eb824..f3f21ddbef 100644 --- a/java/org/apache/catalina/tribes/transport/ReceiverBase.java +++ b/java/org/apache/catalina/tribes/transport/ReceiverBase.java @@ -139,7 +139,10 @@ public abstract class ReceiverBase implements ChannelReceiver, ListenCallback, R } executor = null; if (oname != null) { - JmxRegistry.getRegistry(channel).unregisterJmx(oname); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } oname = null; } channel = null; diff --git a/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java b/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java index dfc9b5a83a..64a729ef07 100644 --- a/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java +++ b/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java @@ -99,7 +99,10 @@ public class ReplicationTransmitter implements ChannelSender { public synchronized void stop() { getTransport().disconnect(); if (oname != null) { - JmxRegistry.getRegistry(channel).unregisterJmx(oname); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } oname = null; } channel = null; diff --git a/test/org/apache/catalina/tribes/jmx/TestJmxRegistry.java b/test/org/apache/catalina/tribes/jmx/TestJmxRegistry.java new file mode 100644 index 0000000000..fa44f85e45 --- /dev/null +++ b/test/org/apache/catalina/tribes/jmx/TestJmxRegistry.java @@ -0,0 +1,103 @@ +/* + * 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.tribes.jmx; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.GroupChannel; + +public class TestJmxRegistry { + + public interface TestInterceptorMBean { + // the presence of the companion interface is what makes the interceptor + // registerable as a Standard MBean, as for the production interceptors + } + + public static class TestInterceptor extends ChannelInterceptorBase implements TestInterceptorMBean { + } + + /* + * A component ObjectName is a stale hint: the registry can be gone when the component stops, + * because it is removed when the channel MBean is deregistered externally. If the JMX + * configuration is still valid, the registry is simply recreated and the MBean unregistered. + */ + @Test + public void testStopRecreatesRegistryWhenStillConfigured() throws Exception { + GroupChannel channel = new GroupChannel(); + channel.setName("TestJmxRegistry-" + System.nanoTime()); + MBeanServer mbserver = ManagementFactory.getPlatformMBeanServer(); + try { + TestInterceptor interceptor = new TestInterceptor(); + interceptor.setChannel(channel); + interceptor.start(Channel.DEFAULT); + ObjectName bean = new ObjectName("ClusterChannel:type=Channel,channel=" + channel.getName() + + ",component=Interceptor,interceptorName=TestInterceptor"); + Assert.assertTrue(mbserver.isRegistered(bean)); + + JmxRegistry.removeRegistry(channel, false); + + interceptor.stop(Channel.DEFAULT); + Assert.assertNull(interceptor.getChannel()); + Assert.assertFalse(mbserver.isRegistered(bean)); + } finally { + JmxRegistry.removeRegistry(channel, true); + } + } + + /* + * If the JMX configuration is invalidated after the registry was removed, the components + * still have to complete their stop and their cleanup. Previously, the missing registry + * surfaced as an uncaught NullPointerException. + */ + @Test + public void testStopWithoutRegistryDoesNotThrow() throws Exception { + GroupChannel channel = new GroupChannel(); + channel.setName("TestJmxRegistry-" + System.nanoTime()); + MBeanServer mbserver = ManagementFactory.getPlatformMBeanServer(); + ObjectName bean = new ObjectName("ClusterChannel:type=Channel,channel=" + channel.getName() + + ",component=Interceptor,interceptorName=TestInterceptor"); + try { + TestInterceptor interceptor = new TestInterceptor(); + interceptor.setChannel(channel); + interceptor.start(Channel.DEFAULT); + Assert.assertTrue(mbserver.isRegistered(bean)); + + // Simulate the channel MBean being deregistered externally, which removes the + // registry, followed by an invalidation of the JMX configuration + JmxRegistry.removeRegistry(channel, false); + channel.setJmxEnabled(false); + + interceptor.stop(Channel.DEFAULT); + Assert.assertNull(interceptor.getChannel()); + } finally { + // Without a registry the stop path cannot unregister the bean any more, clean it up + // here so the shared MBean server is not polluted + if (mbserver.isRegistered(bean)) { + mbserver.unregisterMBean(bean); + } + JmxRegistry.removeRegistry(channel, true); + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
