Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
tls: propagate singleUse to the secure context
tls.connect() sets options.singleUse, and both TLSWrap.prototype.close()
and TLSSocket.prototype._destroySSL() check _secureContext.singleUse
before closing the context. Nothing ever assigned the flag to the
context, so the check never fired and each connection's SSL_CTX stayed
alive until the JS wrapper was garbage collected. The assignment was
lost when the context configuration was extracted into
configSecureContext().

Restore it in createSecureContext(), which owns the JS wrapper the flag
is read from, and make the close idempotent: a socket that swaps its
handle, as autoSelectFamily retries do, shares a single context between
the successive TLSWraps and each of them reaches the same teardown.

Signed-off-by: Carlos Vinicius <viniciusdev.26@gmail.com>
  • Loading branch information
ViniciusDev26 committed Sep 14, 2026
commit e7be7bd7df459034888dc3d9a9379e4356c43598
8 changes: 8 additions & 0 deletions lib/internal/tls/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function createSecureContext(options) {
minVersion,
maxVersion,
secureProtocol,
singleUse,
} = options;

let { secureOptions } = options;
Expand All @@ -113,6 +114,13 @@ function createSecureContext(options) {

configSecureContext(c.context, options);

// A single-use context belongs to exactly one connection, so the underlying
// SSL_CTX can be freed as soon as that connection closes instead of waiting
// for the JS wrapper to be garbage collected. TLSSocket relies on this flag
// to know that it may close the context itself.
if (singleUse)
c.singleUse = true;

return c;
}

Expand Down
23 changes: 15 additions & 8 deletions lib/internal/tls/wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,19 @@ for (const proxiedMethod of proxiedMethods) {
makeMethodProxy(proxiedMethod);
}

// A single-use context is owned by the connection that created it, so its
// SSL_CTX can be released as soon as the connection is gone instead of
// waiting for the JS wrapper to be garbage collected. A socket that swaps
// its handle (autoSelectFamily retries, kReinitializeHandle) shares one
// context between the successive TLSWraps, and the teardown may be reached
// from more than one path, so closing has to be idempotent.
function closeSingleUseContext(secureContext) {
if (secureContext.singleUse && secureContext.context !== null) {
secureContext.context.close();
secureContext.context = null;
}
}

tls_wrap.TLSWrap.prototype.close = function close(cb) {
let ssl;
if (this[owner_symbol]) {
Expand All @@ -760,10 +773,7 @@ tls_wrap.TLSWrap.prototype.close = function close(cb) {
const done = () => {
if (ssl) {
ssl.destroySSL();
if (ssl._secureContext.singleUse) {
ssl._secureContext.context.close();
ssl._secureContext.context = null;
}
closeSingleUseContext(ssl._secureContext);
}
if (cb)
cb();
Expand Down Expand Up @@ -888,10 +898,7 @@ function destroySSL(self) {
TLSSocket.prototype._destroySSL = function _destroySSL() {
if (!this.ssl) return;
this.ssl.destroySSL();
if (this.ssl._secureContext.singleUse) {
this.ssl._secureContext.context.close();
this.ssl._secureContext.context = null;
}
closeSingleUseContext(this.ssl._secureContext);
this.ssl = null;
this[kPendingSession] = null;
this[kIsVerified] = false;
Expand Down
72 changes: 72 additions & 0 deletions test/parallel/test-tls-connect-single-use-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

// tls.connect() builds a SecureContext per connection and marks it as single
// use, so that the underlying SSL_CTX is released as soon as the socket
// closes instead of when the JS wrapper happens to be garbage collected.
// Regression test for https://github.com/nodejs/node/issues/66002, where the
// flag was lost on its way to the context and nothing was ever closed.

const assert = require('assert');
const tls = require('tls');
const fixtures = require('../common/fixtures');

const key = fixtures.readKey('agent1-key.pem');
const cert = fixtures.readKey('agent1-cert.pem');

const server = tls.createServer({ key, cert }, (conn) => conn.end());

server.listen(0, common.mustCall(() => {
connectWithOwnContext(common.mustCall(() => {
connectWithSharedContext(common.mustCall(() => server.close()));
}));
}));

// _destroySSL() runs from the immediate queue, after the 'close' event.
function afterDestroySSL(socket, fn) {
socket.on('close', common.mustCall(() => setImmediate(fn)));
}

function connectWithOwnContext(done) {
const socket = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
}, common.mustCall(() => {
const secureContext = socket.ssl._secureContext;
assert.strictEqual(secureContext.singleUse, true);
assert.notStrictEqual(secureContext.context, null);

afterDestroySSL(socket, common.mustCall(() => {
assert.strictEqual(secureContext.context, null);
done();
}));
}));
}

function connectWithSharedContext(done) {
// A context passed in by the user may outlive the connection, so it must
// not be marked single use, and it must still work for the next socket.
const secureContext = tls.createSecureContext();
let remaining = 2;

(function connectOnce() {
const socket = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
secureContext,
}, common.mustCall(() => {
assert.strictEqual(socket.ssl._secureContext, secureContext);
assert.strictEqual(secureContext.singleUse, undefined);

afterDestroySSL(socket, common.mustCall(() => {
assert.notStrictEqual(secureContext.context, null);
if (--remaining === 0) done();
else connectOnce();
}));
}));
})();
}
Loading