zlib: don't throw when a write is flushed after cleanup - #1506
Open
ArockiaRajamanickam wants to merge 1 commit into
Open
zlib: don't throw when a write is flushed after cleanup#1506ArockiaRajamanickam wants to merge 1 commit into
ArockiaRajamanickam wants to merge 1 commit into
Conversation
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: mscdex#1178 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1178.
ZlibPacketWriter#cleanup()closed the zlib handle but leftthis._zlibpointing at it. Any outbound packet finalized after that reachedthis._zlib.writeSync(...)on a closed handle, and zlib throwsError: Invalid Zlib instancefrom there. It escapes through the socket'close'handler, so there is nothing in userland that can catch it and the process dies.The fix clears the reference in
cleanup()and returns early fromfinalize()when it is gone.Why it returns quietly rather than emitting an error
This was the part worth thinking about, and I went the opposite way from what the issue thread suggested.
Emitting an error here would fire on ordinary, successful teardown. I instrumented
finalize()and saw between 1 and 34 late writes per disconnect, even on healthy connections with compression off. Turning every clean disconnect into an'error'event would be a worse regression than the crash for well-behaved users, and it breakscommon.js's ownsetup()helper, which treats an unexpectederrorevent as a failure.The uncompressed path already handles this exact situation the same way:
crypto.js's cipher classes all return quietly when called afterfree(), andPacketWriter#finalize()has nothing to throw. So returning quietly matches the convention already in the codebase rather than inventing a new one for the compressed path.Nothing that could have reached the wire is discarded. By the time this triggers the socket is gone, the cipher is freed, and
Protocol#_onWritehas been replaced with a thrower — the packet was unsendable either way. The only question was whether it also killed the process.If you would rather it surfaced as an error, I think that belongs in
Protocol#_destruct()for every packet writer, not inzlib.jsalone. Happy to do it that way instead.Test
Added to
test/test-misc-client-server.js: a client and server that negotiatezlib, then callclient._protocol.channelData(...)from the client's'close'handler, which is the moment aftercleanup()when a queued write can still be flushed.Reverting only
lib/protocol/zlib.jsand keeping the test crashes withError: Invalid Zlib instanceat the same three frames as the issue's stack trace; with the fix it passes. It is deterministic — the real-world SFTP path only crashed about half the time, and you asked for a test as minimal as possible, so I drove the same code path directly rather than racing it.Two things to push back on if you disagree:
test-misc-client-server.jsalready does this in several places (conn._protocol.channelSuccess,client._protocol._offer,client._chanMgr._channels) andtest-sftp.jscallssftp._protocol.exitStatus(...), so it is consistent with the suite, but it is still white-box. It assertsinfo.cs.compress === 'zlib'on handshake so it cannot silently stop exercising the compressed path.cleanup()running before'close'is emitted (client.js:812-819). True today and what makes it deterministic, but if that order ever changed the test would quietly stop covering the bug.ZlibPacketReader#read()has the same shape of hazard. I could not construct a path that reaches it, so I left it alone rather than change code I could not exercise — flagging it rather than quietly fixing it.Verification notes
Tested on Node v25.9.0, macOS only; I did not run the full CI matrix.
test-integration-openssh.jsalready fails on a clean checkout in my environment because the host OpenSSH rejects an old RSA key fixture — that is unrelated and unchanged by this branch, so please don't read it as a regression.Also worth knowing: if #1491 (SFTP
cleanupRequestslooping) lands first, it would make this crash more frequent, since it increases the number of late writes during teardown.I used an AI assistant while working on this. The instrumentation counts, the negative control and the decision not to emit an error are mine.