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

lizhimins pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/rocketmq.git


The following commit(s) were added to refs/heads/develop by this push:
     new 78b05467ef [ISSUE #10398] Fix native memory leak on TLS certificate 
hot-reload (#10399)
78b05467ef is described below

commit 78b05467ef354fbd08028bd424c18771f9f4fad9
Author: qianye <[email protected]>
AuthorDate: Mon Jun 1 14:21:29 2026 +0800

    [ISSUE #10398] Fix native memory leak on TLS certificate hot-reload (#10399)
---
 .../proxy/grpc/ProxyAndTlsProtocolNegotiator.java  | 26 +++++++++++++++++++---
 .../remoting/netty/NettyRemotingServer.java        | 22 +++++++++++++++++-
 2 files changed, 44 insertions(+), 4 deletions(-)

diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java
 
b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java
index 4222dacaad..fbe6d84634 100644
--- 
a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java
+++ 
b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java
@@ -45,6 +45,7 @@ import 
io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactor
 import io.grpc.netty.shaded.io.netty.handler.ssl.util.SelfSignedCertificate;
 import io.grpc.netty.shaded.io.netty.util.AsciiString;
 import io.grpc.netty.shaded.io.netty.util.CharsetUtil;
+import io.grpc.netty.shaded.io.netty.util.ReferenceCountUtil;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -77,7 +78,7 @@ public class ProxyAndTlsProtocolNegotiator implements 
InternalProtocolNegotiator
      */
     private static final int SSL_RECORD_HEADER_LENGTH = 5;
 
-    private static SslContext sslContext;
+    private static volatile SslContext sslContext;
 
     public ProxyAndTlsProtocolNegotiator() {
         try {
@@ -113,9 +114,10 @@ public class ProxyAndTlsProtocolNegotiator implements 
InternalProtocolNegotiator
             provider = SslProvider.JDK;
             log.info("Using JDK SSL provider");
         }
+        SslContext newSslContext;
         if (proxyConfig.isTlsTestModeEnable()) {
             SelfSignedCertificate selfSignedCertificate = new 
SelfSignedCertificate();
-            sslContext = 
GrpcSslContexts.forServer(selfSignedCertificate.certificate(), 
selfSignedCertificate.privateKey())
+            newSslContext = 
GrpcSslContexts.forServer(selfSignedCertificate.certificate(), 
selfSignedCertificate.privateKey())
                 .sslProvider(provider)
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                 .clientAuth(ClientAuth.NONE)
@@ -128,7 +130,7 @@ public class ProxyAndTlsProtocolNegotiator implements 
InternalProtocolNegotiator
                 Paths.get(tlsKeyPath));
                  InputStream serverCertificateStream = Files.newInputStream(
                      Paths.get(tlsCertPath))) {
-                sslContext = GrpcSslContexts.forServer(serverCertificateStream,
+                newSslContext = 
GrpcSslContexts.forServer(serverCertificateStream,
                         serverKeyInputStream,
                         StringUtils.isNotBlank(tlsKeyPassword) ? 
tlsKeyPassword : null)
                     .trustManager(InsecureTrustManagerFactory.INSTANCE)
@@ -136,6 +138,24 @@ public class ProxyAndTlsProtocolNegotiator implements 
InternalProtocolNegotiator
                     .build();
             }
         }
+        SslContext oldSslContext = sslContext;
+        sslContext = newSslContext;
+        if (oldSslContext != null) {
+            // Release the old SslContext to free native memory (OpenSSL 
provider only).
+            // ReferenceCountUtil.release() is a no-op for JDK SslContext 
since it does not
+            // implement ReferenceCounted.
+            // Note: there is a theoretical race where an event-loop thread 
could read the old
+            // sslContext (volatile) and call newHandler() after release. In 
practice this is
+            // negligible because cert reload is very infrequent and the 
window is nanoseconds.
+            // Worst case: the single new connection gets an 
IllegalReferenceCountException and
+            // the client retries successfully — no pod crash or service 
disruption.
+            try {
+                ReferenceCountUtil.release(oldSslContext);
+                log.info("Old SslContext released for proxy server");
+            } catch (Exception e) {
+                log.warn("Failed to release old SslContext for proxy server", 
e);
+            }
+        }
     }
 
     private class ProxyAndTlsProtocolHandler extends ByteToMessageDecoder {
diff --git 
a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java
 
b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java
index 578c102daa..ab2e208f48 100644
--- 
a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java
+++ 
b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java
@@ -45,6 +45,7 @@ import io.netty.handler.codec.haproxy.HAProxyMessage;
 import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
 import io.netty.handler.codec.haproxy.HAProxyProtocolVersion;
 import io.netty.handler.codec.haproxy.HAProxyTLV;
+import io.netty.handler.ssl.SslContext;
 import io.netty.handler.timeout.IdleState;
 import io.netty.handler.timeout.IdleStateEvent;
 import io.netty.handler.timeout.IdleStateHandler;
@@ -53,6 +54,7 @@ import io.netty.util.CharsetUtil;
 import io.netty.util.HashedWheelTimer;
 import io.netty.util.Timeout;
 import io.netty.util.TimerTask;
+import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.DefaultEventExecutorGroup;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -183,7 +185,25 @@ public class NettyRemotingServer extends 
NettyRemotingAbstract implements Remoti
 
         if (tlsMode != TlsMode.DISABLED) {
             try {
-                sslContext = TlsHelper.buildSslContext(false);
+                SslContext newSslContext = TlsHelper.buildSslContext(false);
+                SslContext oldSslContext = this.sslContext;
+                this.sslContext = newSslContext;
+                if (oldSslContext != null) {
+                    // Release the old SslContext to free native memory 
(OpenSSL provider only).
+                    // ReferenceCountUtil.release() is a no-op for JDK 
SslContext since it does not
+                    // implement ReferenceCounted.
+                    // Note: there is a theoretical race where an event-loop 
thread could read the old
+                    // sslContext (volatile) and call newHandler() after 
release. In practice this is
+                    // negligible because cert reload is very infrequent and 
the window is nanoseconds.
+                    // Worst case: the single new connection gets an 
IllegalReferenceCountException and
+                    // the client retries successfully — no pod crash or 
service disruption.
+                    try {
+                        ReferenceCountUtil.release(oldSslContext);
+                        log.info("Old SslContext released for server");
+                    } catch (Exception e) {
+                        log.warn("Failed to release old SslContext for 
server", e);
+                    }
+                }
                 log.info("SslContext created for server");
             } catch (CertificateException | IOException e) {
                 log.error("Failed to create SslContext for server", e);

Reply via email to