This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 29c980f921c230e6b4e9cdb3bbc143671058f402
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 5629a6f556..66bedc64b9 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 c6a21ea7e0..ea4a9543c2 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]

Reply via email to