From 72ff6143cb1da45d85cbc0c03b17fd7d2a7264ad Mon Sep 17 00:00:00 2001 From: ArockiaRajamanickam Date: Sat, 1 Aug 2026 17:14:50 +0530 Subject: [PATCH] zlib,test: don't throw when a write is flushed after cleanup Protocol#_destruct() closes the zlib instances used by ZlibPacketWriter, but outbound data can still be flushed afterwards -- for example when SFTP's cleanupRequests() runs during connection teardown and a request callback synchronously issues another write. In that case ZlibPacketWriter#finalize() called into a closed Zlib instance, which threw 'Invalid Zlib instance' from an asynchronous callback where nothing could catch it, taking down the process. Ciphers already tolerate this: free() marks them dead and encrypt() returns early instead of throwing. Do the same for the compressing packet writer, so a late write is dropped rather than thrown. This matches the behaviour already seen with compression disabled, where the same late writes are silently discarded by the freed cipher. Fixes: https://github.com/mscdex/ssh2/issues/1178 Co-Authored-By: Claude Fable 5 --- lib/protocol/zlib.js | 11 ++++++++++- test/test-misc-client-server.js | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/protocol/zlib.js b/lib/protocol/zlib.js index f68319a1..feb61e55 100644 --- a/lib/protocol/zlib.js +++ b/lib/protocol/zlib.js @@ -177,8 +177,10 @@ class ZlibPacketWriter { } cleanup() { - if (this._zlib) + if (this._zlib) { _close(this._zlib); + this._zlib = undefined; + } } alloc(payloadSize, force) { @@ -186,6 +188,13 @@ class ZlibPacketWriter { } finalize(payload, force) { + // A write may still be flushed after cleanup (e.g. a queued channel + // request being flushed while the connection is being torn down), in + // which case there is nothing left to compress or send + + if (this._zlib === undefined) + return; + if (this._protocol._kexinit === undefined || force) { const output = this._zlib.writeSync(payload, true); const packet = this._protocol._cipher.allocPacket(output.totalLen); diff --git a/test/test-misc-client-server.js b/test/test-misc-client-server.js index 2dd5a29d..63b30d45 100644 --- a/test/test-misc-client-server.js +++ b/test/test-misc-client-server.js @@ -1458,3 +1458,37 @@ const setup = setupSimple.bind(undefined, debug); })); })); } + +{ + const { client, server } = setup_( + 'Outbound data flushed after cleanup should not throw (compression)', + { + client: { + ...clientCfg, + algorithms: { compress: [ 'zlib' ] }, + }, + server: { + ...serverCfg, + algorithms: { compress: [ 'zlib' ] }, + }, + }, + ); + + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { + ctx.accept(); + })).on('ready', mustCall(() => {})); + })); + + client.on('handshake', mustCall((info) => { + assert(info.cs.compress === 'zlib', 'wrong compress value'); + })).on('ready', mustCall(() => { + client.end(); + })).on('close', mustCall(() => { + // The protocol instance (and with it the zlib instances) is cleaned up + // before this event is emitted. Data that was still queued when the + // connection went away can get flushed after that point, and doing so + // must not throw, since there is nothing that could catch it. + client._protocol.channelData(0, Buffer.from('foo')); + })); +}