Skip to content

Commit 78b0546

Browse files
authored
[ISSUE #10398] Fix native memory leak on TLS certificate hot-reload (#10399)
1 parent 8561729 commit 78b0546

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import io.grpc.netty.shaded.io.netty.handler.ssl.util.SelfSignedCertificate;
4646
import io.grpc.netty.shaded.io.netty.util.AsciiString;
4747
import io.grpc.netty.shaded.io.netty.util.CharsetUtil;
48+
import io.grpc.netty.shaded.io.netty.util.ReferenceCountUtil;
4849

4950
import java.io.IOException;
5051
import java.io.InputStream;
@@ -77,7 +78,7 @@ public class ProxyAndTlsProtocolNegotiator implements InternalProtocolNegotiator
7778
*/
7879
private static final int SSL_RECORD_HEADER_LENGTH = 5;
7980

80-
private static SslContext sslContext;
81+
private static volatile SslContext sslContext;
8182

8283
public ProxyAndTlsProtocolNegotiator() {
8384
try {
@@ -113,9 +114,10 @@ public static void loadSslContext() throws CertificateException, IOException {
113114
provider = SslProvider.JDK;
114115
log.info("Using JDK SSL provider");
115116
}
117+
SslContext newSslContext;
116118
if (proxyConfig.isTlsTestModeEnable()) {
117119
SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
118-
sslContext = GrpcSslContexts.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey())
120+
newSslContext = GrpcSslContexts.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey())
119121
.sslProvider(provider)
120122
.trustManager(InsecureTrustManagerFactory.INSTANCE)
121123
.clientAuth(ClientAuth.NONE)
@@ -128,14 +130,32 @@ public static void loadSslContext() throws CertificateException, IOException {
128130
Paths.get(tlsKeyPath));
129131
InputStream serverCertificateStream = Files.newInputStream(
130132
Paths.get(tlsCertPath))) {
131-
sslContext = GrpcSslContexts.forServer(serverCertificateStream,
133+
newSslContext = GrpcSslContexts.forServer(serverCertificateStream,
132134
serverKeyInputStream,
133135
StringUtils.isNotBlank(tlsKeyPassword) ? tlsKeyPassword : null)
134136
.trustManager(InsecureTrustManagerFactory.INSTANCE)
135137
.clientAuth(ClientAuth.NONE)
136138
.build();
137139
}
138140
}
141+
SslContext oldSslContext = sslContext;
142+
sslContext = newSslContext;
143+
if (oldSslContext != null) {
144+
// Release the old SslContext to free native memory (OpenSSL provider only).
145+
// ReferenceCountUtil.release() is a no-op for JDK SslContext since it does not
146+
// implement ReferenceCounted.
147+
// Note: there is a theoretical race where an event-loop thread could read the old
148+
// sslContext (volatile) and call newHandler() after release. In practice this is
149+
// negligible because cert reload is very infrequent and the window is nanoseconds.
150+
// Worst case: the single new connection gets an IllegalReferenceCountException and
151+
// the client retries successfully — no pod crash or service disruption.
152+
try {
153+
ReferenceCountUtil.release(oldSslContext);
154+
log.info("Old SslContext released for proxy server");
155+
} catch (Exception e) {
156+
log.warn("Failed to release old SslContext for proxy server", e);
157+
}
158+
}
139159
}
140160

141161
private class ProxyAndTlsProtocolHandler extends ByteToMessageDecoder {

remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
4646
import io.netty.handler.codec.haproxy.HAProxyProtocolVersion;
4747
import io.netty.handler.codec.haproxy.HAProxyTLV;
48+
import io.netty.handler.ssl.SslContext;
4849
import io.netty.handler.timeout.IdleState;
4950
import io.netty.handler.timeout.IdleStateEvent;
5051
import io.netty.handler.timeout.IdleStateHandler;
@@ -53,6 +54,7 @@
5354
import io.netty.util.HashedWheelTimer;
5455
import io.netty.util.Timeout;
5556
import io.netty.util.TimerTask;
57+
import io.netty.util.ReferenceCountUtil;
5658
import io.netty.util.concurrent.DefaultEventExecutorGroup;
5759
import org.apache.commons.collections.CollectionUtils;
5860
import org.apache.commons.lang3.StringUtils;
@@ -183,7 +185,25 @@ public void loadSslContext() {
183185

184186
if (tlsMode != TlsMode.DISABLED) {
185187
try {
186-
sslContext = TlsHelper.buildSslContext(false);
188+
SslContext newSslContext = TlsHelper.buildSslContext(false);
189+
SslContext oldSslContext = this.sslContext;
190+
this.sslContext = newSslContext;
191+
if (oldSslContext != null) {
192+
// Release the old SslContext to free native memory (OpenSSL provider only).
193+
// ReferenceCountUtil.release() is a no-op for JDK SslContext since it does not
194+
// implement ReferenceCounted.
195+
// Note: there is a theoretical race where an event-loop thread could read the old
196+
// sslContext (volatile) and call newHandler() after release. In practice this is
197+
// negligible because cert reload is very infrequent and the window is nanoseconds.
198+
// Worst case: the single new connection gets an IllegalReferenceCountException and
199+
// the client retries successfully — no pod crash or service disruption.
200+
try {
201+
ReferenceCountUtil.release(oldSslContext);
202+
log.info("Old SslContext released for server");
203+
} catch (Exception e) {
204+
log.warn("Failed to release old SslContext for server", e);
205+
}
206+
}
187207
log.info("SslContext created for server");
188208
} catch (CertificateException | IOException e) {
189209
log.error("Failed to create SslContext for server", e);

0 commit comments

Comments
 (0)