Skip to content

Commit 24e0144

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 24e0144

3 files changed

Lines changed: 192 additions & 42 deletions

File tree

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

Lines changed: 138 additions & 42 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.
@@ -1739,28 +1824,16 @@ private void cleanupLoggingSocket() {
17391824
}
17401825

17411826
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-
}
1827+
freeEngineResources(ssl_fd, read_buf, write_buf);
1828+
ssl_fd = null;
1829+
read_buf = null;
1830+
write_buf = null;
17601831

1761-
if (write_buf != null) {
1762-
Buffer.Free(write_buf);
1763-
write_buf = null;
1832+
// Deregister the Cleaner, EngineCleanup.run will be a no-op
1833+
// since freeEngineResources() already freed everything above.
1834+
if (cleanable != null) {
1835+
cleanable.clean();
1836+
cleanable = null;
17641837
}
17651838
}
17661839

@@ -1769,12 +1842,27 @@ private void cleanupSSLFD() {
17691842
// (and its global ref), read_buf, and write_buf.
17701843
@Override
17711844
protected void finalize() {
1772-
cleanup();
1845+
// Intentionally empty — prevents NativeProxy.finalize() from racing
1846+
// with the Cleaner. Resource cleanup is handled by cleanup() or the
1847+
// Cleaner's EngineCleanup.run(). See cleanupSSLFD() and EngineCleanup.
17731848
}
17741849

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

17801868
public String findAuthType(SSLFDProxy ssl_fd, PK11Cert[] chain) throws Exception {
@@ -1851,13 +1939,18 @@ public String findAuthType(SSLFDProxy ssl_fd, PK11Cert[] chain) throws Exception
18511939
@Override
18521940
public int check(SSLFDProxy fd) {
18531941
// Needs to be available for assignException() below.
1942+
1943+
JSSEngineReferenceImpl engine = engineRef.get();
1944+
18541945
PK11Cert[] chain = null;
18551946
String authType;
18561947

18571948
try {
18581949
chain = SSL.PeerCertificateChain(fd);
18591950
authType = findAuthType(fd, chain);
1860-
debug("CertAuthType: " + authType);
1951+
if (engine != null) {
1952+
engine.debug("CertAuthType: " + authType);
1953+
}
18611954

18621955
if (chain == null || chain.length == 0) {
18631956
// When the chain is NULL, we'd always fail in the
@@ -1869,12 +1962,14 @@ public int check(SSLFDProxy fd) {
18691962
// Since we're a server validating the client's
18701963
// chain (and they didn't provide one), we should
18711964
// ignore it instead of forcing the problem.
1872-
debug("No client certificate chain and client cert not needed.");
1965+
if (engine != null) {
1966+
engine.debug("No client certificate chain and client cert not needed.");
1967+
}
18731968
return 0;
18741969
}
18751970
}
18761971
} catch (Exception excpt) {
1877-
return assignException(excpt, chain);
1972+
return assignException(excpt, chain, engine);
18781973
}
18791974

18801975
try {
@@ -1886,9 +1981,9 @@ public int check(SSLFDProxy fd) {
18861981
if (tm instanceof X509ExtendedTrustManager) {
18871982
X509ExtendedTrustManager etm = (X509ExtendedTrustManager) tm;
18881983
if (as_server) {
1889-
etm.checkClientTrusted(chain, authType, JSSEngineReferenceImpl.this);
1984+
etm.checkClientTrusted(chain, authType, engine);
18901985
} else {
1891-
etm.checkServerTrusted(chain, authType, JSSEngineReferenceImpl.this);
1986+
etm.checkServerTrusted(chain, authType, engine);
18921987
}
18931988
} else {
18941989
if (as_server) {
@@ -1899,16 +1994,16 @@ public int check(SSLFDProxy fd) {
18991994
}
19001995
}
19011996
} catch (CertificateException excpt) {
1902-
return handleCertificateException(excpt, chain);
1997+
return handleCertificateException(excpt, chain, engine);
19031998
}
19041999

19052000
return 0;
19062001
}
19072002

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

1911-
if (seen_exception) {
2006+
if (engine == null || engine.isSeenException()) {
19122007
return nss_code;
19132008
}
19142009

@@ -1935,29 +2030,30 @@ private int assignException(Exception excpt, PK11Cert[] chain) {
19352030
}
19362031
msg += "exception message: " + excpt.getMessage();
19372032

1938-
seen_exception = true;
1939-
ssl_exception = new SSLException(msg, excpt);
2033+
engine.setSeenException(true);
2034+
engine.setSslException(new SSLException(msg, excpt));
19402035
return nss_code;
19412036
}
19422037

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

1946-
if (seen_exception) {
2041+
if (engine == null || engine.isSeenException()) {
19472042
return nss_code;
19482043
}
19492044

19502045
String msg = "Unable to validate "
19512046
+ chain[0].getSubjectX500Principal() + ": "
19522047
+ excpt.getMessage();
19532048

1954-
seen_exception = true;
1955-
ssl_exception = new SSLPeerUnverifiedException(msg);
2049+
engine.setSeenException(true);
2050+
engine.setSslException(new SSLPeerUnverifiedException(msg));
2051+
19562052
return nss_code;
19572053
}
19582054
}
19592055

1960-
private class BypassBadHostname extends BadCertHandler {
2056+
private static class BypassBadHostname extends BadCertHandler {
19612057
public BypassBadHostname(SSLFDProxy fd, int error) {
19622058
super(fd, error);
19632059
}

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)