Skip to content

Commit 7903dbe

Browse files
committed
First cut of using Cleaner object to eliminate the SSLEngine finalizer problem.
Assisted-by: claude Sonnet 4.5.
1 parent ce561a0 commit 7903dbe

3 files changed

Lines changed: 195 additions & 44 deletions

File tree

base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java

Lines changed: 141 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import java.security.cert.CertificateException;
1313
import java.util.ArrayList;
1414
import java.util.List;
15+
import java.util.concurrent.atomic.AtomicInteger;
1516

1617
import javax.net.ssl.SSLEngineResult;
1718
import javax.net.ssl.SSLException;
19+
import java.lang.ref.Cleaner;
1820
import javax.net.ssl.SSLHandshakeException;
1921
import javax.net.ssl.SSLPeerUnverifiedException;
2022
import javax.net.ssl.X509ExtendedTrustManager;
@@ -61,6 +63,46 @@
6163
* as being from the appropriate side of the TLS connection.
6264
*/
6365
public class JSSEngineReferenceImpl extends JSSEngine {
66+
67+
/**
68+
* Cleaner instance for guaranteed resource cleanup.
69+
* Used as a safety net when close()/cleanup() is not called explicitly.
70+
*/
71+
private static final Cleaner cleaner = Cleaner.create();
72+
private Cleaner.Cleanable cleanable;
73+
74+
// Used for further identifying the engine instance being cleaned.
75+
private static java.util.concurrent.atomic.AtomicInteger engineCounter = new AtomicInteger(0);
76+
private int engineId = engineCounter.incrementAndGet();
77+
78+
79+
/**
80+
* Cleaner action - must be a static class to avoid preventing GC
81+
* of the JSSEngineReferenceImpl instance.
82+
*/
83+
private static class EngineCleanup implements Runnable {
84+
private final String name;
85+
86+
private SSLFDProxy ssl_fd;
87+
private BufferProxy read_buf;
88+
private BufferProxy write_buf;
89+
90+
EngineCleanup(String name, SSLFDProxy fd, BufferProxy rb, BufferProxy wb) {
91+
this.name = name;
92+
this.ssl_fd = fd;
93+
this.read_buf = rb;
94+
this.write_buf = wb;
95+
}
96+
97+
public void run() {
98+
logger.debug("JSSEngine: CLEANER: EngineCleanup.run() fired for " + name);
99+
freeEngineResources(ssl_fd, read_buf, write_buf);
100+
ssl_fd = null;
101+
read_buf = null;
102+
write_buf = null;
103+
}
104+
}
105+
64106
/**
65107
* Faked peer information that we pass to the underlying BufferPRFD
66108
* implementation.
@@ -224,7 +266,7 @@ public JSSEngineReferenceImpl(String peerHost, int peerPort,
224266
debug("JSSEngine: constructor(" + peerHost + ", " + peerPort + ", " + localCert + ", " + localKey + ")");
225267
}
226268

227-
private void debug(String msg) {
269+
void debug(String msg) {
228270
logger.debug(prefix + msg);
229271
}
230272

@@ -236,6 +278,40 @@ private void warn(String msg) {
236278
logger.warn(prefix + msg);
237279
}
238280

281+
private static void freeEngineResources(SSLFDProxy fd, BufferProxy rb, BufferProxy wb) {
282+
if (fd != null && !fd.isNull()) {
283+
try {
284+
SSL.RemoveCallbacks(fd);
285+
fd.close();
286+
} catch (Exception e) {
287+
logger.error("Error closing ssl_fd", e);
288+
}
289+
}
290+
if (rb != null && !rb.isNull()) {
291+
Buffer.Free(rb);
292+
}
293+
if (wb != null && !wb.isNull()) {
294+
Buffer.Free(wb);
295+
}
296+
}
297+
298+
299+
boolean isSeenException() {
300+
return seen_exception;
301+
}
302+
303+
void setSeenException(boolean val) {
304+
seen_exception = val;
305+
}
306+
307+
SSLException getSslException() {
308+
return ssl_exception;
309+
}
310+
311+
void setSslException(SSLException e) {
312+
ssl_exception = e;
313+
}
314+
239315
/**
240316
* Set the name of this JSSEngine instance, to be printed in logging calls.
241317
*
@@ -349,6 +425,11 @@ private void createBufferFD() throws SSLException {
349425
fd = null;
350426
closed_fd = false;
351427

428+
cleanable = cleaner.register(this, new EngineCleanup(
429+
"engine-" + engineId + " " + peer_info, ssl_fd, read_buf, write_buf));
430+
431+
logger.debug("Registering cleaner: " + "engine-" + engineId + " " + peer_info);
432+
352433
// Turn on SSL Alert Logging for the ssl_fd object.
353434
int ret = SSL.EnableAlertLogging(ssl_fd);
354435
if (ret == SSL.SECFailure) {
@@ -588,7 +669,7 @@ private void applyTrustManagers() throws SSLException {
588669
// from Runnable, so we can reuse it here as well. We can create
589670
// it ahead of time though. In this case, checkNeedCertValidation()
590671
// is never called.
591-
ssl_fd.certAuthHandler = new CertValidationTask(ssl_fd);
672+
ssl_fd.certAuthHandler = new CertValidationTask(ssl_fd, as_server, need_client_auth, trust_managers, this);
592673

593674
if (SSL.ConfigSyncTrustManagerCertAuthCallback(ssl_fd) == SSL.SECFailure) {
594675
throw new SSLException("Unable to configure TrustManager validation on this JSSengine: " + errorText(PR.GetError()));
@@ -842,7 +923,7 @@ private boolean checkNeedCertValidation() {
842923
debug("JSSEngine: checkNeedCertValidation() - creating task");
843924

844925
// OK, time to create our runnable task.
845-
task = new CertValidationTask(ssl_fd);
926+
task = new CertValidationTask(ssl_fd, as_server, need_client_auth, trust_managers, this);
846927

847928
// Update our handshake state so we know what to do next.
848929
handshake_state = SSLEngineResult.HandshakeStatus.NEED_TASK;
@@ -1686,6 +1767,10 @@ public synchronized void tryCleanup() {
16861767
}
16871768
}
16881769

1770+
public int getEngineId() {
1771+
return engineId;
1772+
}
1773+
16891774
/**
16901775
* Performs cleanup of internal data, closing both inbound and outbound
16911776
* data streams if still open.
@@ -1720,6 +1805,13 @@ public synchronized void cleanup() {
17201805
session.close();
17211806
session = null;
17221807
}
1808+
1809+
// Deregister the Cleaner. clean() runs EngineCleanup.run()
1810+
// which is a no-op since cleanupSSLFD() already freed everything.
1811+
if (cleanable != null) {
1812+
cleanable.clean();
1813+
cleanable = null;
1814+
}
17231815
}
17241816

17251817
private void cleanupLoggingSocket() {
@@ -1739,42 +1831,39 @@ private void cleanupLoggingSocket() {
17391831
}
17401832

17411833
private void cleanupSSLFD() {
1742-
if (ssl_fd != null) {
1743-
// closed_fd is already set to true in cleanup() before this is called.
1744-
// This prevents concurrent calls to closeInbound()/closeOutbound() from
1745-
// attempting PR.Shutdown() on ssl_fd that is being freed.
1746-
try {
1747-
SSL.RemoveCallbacks(ssl_fd);
1748-
ssl_fd.close();
1749-
} catch (Exception e) {
1750-
logger.error("Got exception trying to cleanup SSLFD", e);
1751-
} finally {
1752-
ssl_fd = null;
1753-
}
1754-
}
1755-
1756-
if (read_buf != null) {
1757-
Buffer.Free(read_buf);
1758-
read_buf = null;
1759-
}
1760-
1761-
if (write_buf != null) {
1762-
Buffer.Free(write_buf);
1763-
write_buf = null;
1764-
}
1834+
freeEngineResources(ssl_fd, read_buf, write_buf);
1835+
ssl_fd = null;
1836+
read_buf = null;
1837+
write_buf = null;
17651838
}
17661839

17671840
// During testing with Tomcat 8.5, most instances did not call
17681841
// cleanup, so all the JNI resources end up getting leaked: ssl_fd
17691842
// (and its global ref), read_buf, and write_buf.
17701843
@Override
17711844
protected void finalize() {
1772-
cleanup();
1845+
// For now clear this while implementing the Cleaner class based solution
1846+
1847+
logger.debug("JSSEngine: CLEANER: finalize() called - no-op, Cleaner handles cleanup");
1848+
//cleanup();
17731849
}
17741850

1775-
private class CertValidationTask extends CertAuthHandler {
1776-
public CertValidationTask(SSLFDProxy fd) {
1851+
private static class CertValidationTask extends CertAuthHandler {
1852+
private final java.lang.ref.WeakReference<JSSEngineReferenceImpl> engineRef;
1853+
private final boolean as_server;
1854+
private final boolean need_client_auth;
1855+
private final X509TrustManager[] trust_managers;
1856+
1857+
public CertValidationTask(SSLFDProxy fd, boolean asServer,
1858+
boolean needClientAuth, X509TrustManager[] trustManagers,
1859+
JSSEngineReferenceImpl engine) {
1860+
17771861
super(fd);
1862+
this.as_server = asServer;
1863+
this.need_client_auth = needClientAuth;
1864+
this.trust_managers = trustManagers;
1865+
this.engineRef = new java.lang.ref.WeakReference<>(engine);
1866+
17781867
}
17791868

17801869
public String findAuthType(SSLFDProxy ssl_fd, PK11Cert[] chain) throws Exception {
@@ -1851,13 +1940,18 @@ public String findAuthType(SSLFDProxy ssl_fd, PK11Cert[] chain) throws Exception
18511940
@Override
18521941
public int check(SSLFDProxy fd) {
18531942
// Needs to be available for assignException() below.
1943+
1944+
JSSEngineReferenceImpl engine = engineRef.get();
1945+
18541946
PK11Cert[] chain = null;
18551947
String authType;
18561948

18571949
try {
18581950
chain = SSL.PeerCertificateChain(fd);
18591951
authType = findAuthType(fd, chain);
1860-
debug("CertAuthType: " + authType);
1952+
if (engine != null) {
1953+
engine.debug("CertAuthType: " + authType);
1954+
}
18611955

18621956
if (chain == null || chain.length == 0) {
18631957
// When the chain is NULL, we'd always fail in the
@@ -1869,12 +1963,14 @@ public int check(SSLFDProxy fd) {
18691963
// Since we're a server validating the client's
18701964
// chain (and they didn't provide one), we should
18711965
// ignore it instead of forcing the problem.
1872-
debug("No client certificate chain and client cert not needed.");
1966+
if (engine != null) {
1967+
engine.debug("No client certificate chain and client cert not needed.");
1968+
}
18731969
return 0;
18741970
}
18751971
}
18761972
} catch (Exception excpt) {
1877-
return assignException(excpt, chain);
1973+
return assignException(excpt, chain, engine);
18781974
}
18791975

18801976
try {
@@ -1886,9 +1982,9 @@ public int check(SSLFDProxy fd) {
18861982
if (tm instanceof X509ExtendedTrustManager) {
18871983
X509ExtendedTrustManager etm = (X509ExtendedTrustManager) tm;
18881984
if (as_server) {
1889-
etm.checkClientTrusted(chain, authType, JSSEngineReferenceImpl.this);
1985+
etm.checkClientTrusted(chain, authType, engine);
18901986
} else {
1891-
etm.checkServerTrusted(chain, authType, JSSEngineReferenceImpl.this);
1987+
etm.checkServerTrusted(chain, authType, engine);
18921988
}
18931989
} else {
18941990
if (as_server) {
@@ -1899,16 +1995,16 @@ public int check(SSLFDProxy fd) {
18991995
}
19001996
}
19011997
} catch (CertificateException excpt) {
1902-
return handleCertificateException(excpt, chain);
1998+
return handleCertificateException(excpt, chain, engine);
19031999
}
19042000

19052001
return 0;
19062002
}
19072003

1908-
private int assignException(Exception excpt, PK11Cert[] chain) {
2004+
private int assignException(Exception excpt, PK11Cert[] chain, JSSEngineReferenceImpl engine) {
19092005
int nss_code = Cert.MatchExceptionToNSSError(excpt);
19102006

1911-
if (seen_exception) {
2007+
if (engine == null || engine.isSeenException()) {
19122008
return nss_code;
19132009
}
19142010

@@ -1935,29 +2031,30 @@ private int assignException(Exception excpt, PK11Cert[] chain) {
19352031
}
19362032
msg += "exception message: " + excpt.getMessage();
19372033

1938-
seen_exception = true;
1939-
ssl_exception = new SSLException(msg, excpt);
2034+
engine.setSeenException(true);
2035+
engine.setSslException(new SSLException(msg, excpt));
19402036
return nss_code;
19412037
}
19422038

1943-
private int handleCertificateException(Exception excpt, PK11Cert[] chain) {
2039+
private int handleCertificateException(Exception excpt, PK11Cert[] chain, JSSEngineReferenceImpl engine) {
19442040
int nss_code = Cert.MatchExceptionToNSSError(excpt);
19452041

1946-
if (seen_exception) {
2042+
if (engine == null || engine.isSeenException()) {
19472043
return nss_code;
19482044
}
19492045

19502046
String msg = "Unable to validate "
19512047
+ chain[0].getSubjectX500Principal() + ": "
19522048
+ excpt.getMessage();
19532049

1954-
seen_exception = true;
1955-
ssl_exception = new SSLPeerUnverifiedException(msg);
2050+
engine.setSeenException(true);
2051+
engine.setSslException(new SSLPeerUnverifiedException(msg));
2052+
19562053
return nss_code;
19572054
}
19582055
}
19592056

1960-
private class BypassBadHostname extends BadCertHandler {
2057+
private static class BypassBadHostname extends BadCertHandler {
19612058
public BypassBadHostname(SSLFDProxy fd, int error) {
19622059
super(fd, error);
19632060
}

base/src/test/java/org/mozilla/jss/tests/TestSSLEngine.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,45 @@ public static void testNativeClientServer(String[] args) throws Exception {
10061006
testJSSEToJSSHandshakes(ctx, server_alias);
10071007
}
10081008

1009+
public static void testCleanerAbandoned(SSLContext ctx, String client_alias, String server_alias) throws Exception {
1010+
JSSEngine client_eng = (JSSEngine) ctx.createSSLEngine();
1011+
client_eng.setSSLParameters(createParameters(client_alias));
1012+
client_eng.setUseClientMode(true);
1013+
1014+
JSSEngine server_eng = (JSSEngine) ctx.createSSLEngine();
1015+
server_eng.setSSLParameters(createParameters(server_alias));
1016+
server_eng.setUseClientMode(false);
1017+
server_eng.setNeedClientAuth(true);
1018+
1019+
configureSSLEngine(client_eng, "TLSv1.2", client_eng.getSupportedCipherSuites()[0]);
1020+
configureSSLEngine(server_eng, "TLSv1.2", server_eng.getSupportedCipherSuites()[0]);
1021+
1022+
testHandshake(client_eng, server_eng, false);
1023+
1024+
// Abandon both engines — no close, no cleanup
1025+
1026+
java.lang.ref.WeakReference<JSSEngine> clientRef = new java.lang.ref.WeakReference<>(client_eng);
1027+
java.lang.ref.WeakReference<JSSEngine> serverRef = new java.lang.ref.WeakReference<>(server_eng);
1028+
1029+
// Print engine IDs before abandoning
1030+
System.out.println("Abandoning client engine: " + ((JSSEngineReferenceImpl) client_eng).getEngineId());
1031+
System.out.println("Abandoning server engine (with certAuthHandler): " + ((JSSEngineReferenceImpl) server_eng).getEngineId());
1032+
1033+
client_eng = null;
1034+
server_eng = null;
1035+
1036+
System.gc();
1037+
Thread.sleep(1000);
1038+
System.gc();
1039+
System.runFinalization();
1040+
Thread.sleep(1000);
1041+
1042+
System.out.println("Client engine collected: " + (clientRef.get() == null));
1043+
System.out.println("Server engine collected: " + (serverRef.get() == null));
1044+
1045+
System.out.println("testCleanerAbandoned completed — no crash!");
1046+
}
1047+
10091048
public static void main(String[] args) throws Exception {
10101049
// Args:
10111050
// - nssdb
@@ -1022,6 +1061,14 @@ public static void main(String[] args) throws Exception {
10221061

10231062
assert(SSLVersion.TLS_1_2.matchesAlias("TLSv1.2"));
10241063

1064+
if (args.length > 4 && args[4].equals("cleaner_abandoned")) {
1065+
System.out.println("Testing Cleaner with abandoned engine...");
1066+
SSLContext ctx = SSLContext.getInstance("TLS", "Mozilla-JSS");
1067+
ctx.init(getKMs(), getTMs(), null);
1068+
testCleanerAbandoned(ctx, args[2], args[3]);
1069+
return;
1070+
}
1071+
10251072
System.out.println("Testing provided instance...");
10261073
testProvided();
10271074

0 commit comments

Comments
 (0)