diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index b36b90c54ca233..96099388529c20 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -305,14 +305,31 @@ proxiedMethods.forEach(function(name) { }); tls_wrap.TLSWrap.prototype.close = function close(cb) { - if (this.owner) + let ssl; + if (this.owner) { + ssl = this.owner.ssl; this.owner.ssl = null; + } + + // Invoke `destroySSL` on close to clean up possibly pending write requests + // that may self-reference TLSWrap, leading to leak + const done = () => { + if (ssl) { + ssl.destroySSL(); + if (ssl._secureContext.singleUse) { + ssl._secureContext.context.close(); + ssl._secureContext.context = null; + } + } + if (cb) + cb(); + }; if (this._parentWrap && this._parentWrap._handle === this._parent) { - this._parentWrap.once('close', cb); + this._parentWrap.once('close', done); return this._parentWrap.destroy(); } - return this._parent.close(cb); + return this._parent.close(done); }; TLSSocket.prototype._wrapHandle = function(wrap) { diff --git a/test/parallel/test-tls-writewrap-leak.js b/test/parallel/test-tls-writewrap-leak.js new file mode 100644 index 00000000000000..cc55192229531d --- /dev/null +++ b/test/parallel/test-tls-writewrap-leak.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const assert = require('assert'); +const net = require('net'); +const tls = require('tls'); + +const server = net.createServer(common.mustCall((c) => { + c.destroy(); +})).listen(0, common.mustCall(() => { + const c = tls.connect({ port: server.address().port }); + c.on('error', () => { + // Otherwise `.write()` callback won't be invoked. + c.destroyed = false; + }); + + c.write('hello', common.mustCall((err) => { + assert.equal(err.code, 'ECANCELED'); + server.close(); + })); +}));