zlib: fix use-after-free when reset() is called during write#62325
Merged
nodejs-github-bot merged 1 commit intonodejs:mainfrom Mar 26, 2026
Merged
zlib: fix use-after-free when reset() is called during write#62325nodejs-github-bot merged 1 commit intonodejs:mainfrom
nodejs-github-bot merged 1 commit intonodejs:mainfrom
Conversation
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #62325 +/- ##
=======================================
Coverage 89.68% 89.68%
=======================================
Files 676 676
Lines 206575 206582 +7
Branches 39547 39558 +11
=======================================
+ Hits 185261 185270 +9
- Misses 13443 13445 +2
+ Partials 7871 7867 -4
🚀 New features to boost your workflow:
|
addaleax
approved these changes
Mar 18, 2026
jasnell
approved these changes
Mar 18, 2026
Collaborator
Collaborator
anonrig
approved these changes
Mar 20, 2026
lpinca
approved these changes
Mar 20, 2026
Collaborator
Collaborator
Collaborator
|
Landed in 53bcd11 |
VaishnavIUpadyaya
pushed a commit
to VaishnavIUpadyaya/node
that referenced
this pull request
Mar 27, 2026
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132 PR-URL: nodejs#62325 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
aduh95
pushed a commit
that referenced
this pull request
Mar 28, 2026
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132 PR-URL: #62325 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
aduh95
pushed a commit
that referenced
this pull request
Mar 28, 2026
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132 PR-URL: #62325 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
aduh95
pushed a commit
that referenced
this pull request
Mar 28, 2026
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132 PR-URL: #62325 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Qard
pushed a commit
to Qard/node
that referenced
this pull request
Mar 29, 2026
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132 PR-URL: nodejs#62325 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
aduh95
pushed a commit
to Flarna/node
that referenced
this pull request
Mar 30, 2026
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132 PR-URL: nodejs#62325 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
RafaelGSS
pushed a commit
to RafaelGSS/node
that referenced
this pull request
Mar 30, 2026
The Reset() method did not check the write_in_progress_ flag before resetting the compression stream. This allowed reset() to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free. Add a write_in_progress_ guard to Reset() that throws an error if a write is in progress, matching the existing pattern used by Close() and Write(). PR-URL: TODO Refs: https://hackerone.com/reports/3609132 PR-URL: nodejs#62325 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
rom1504
added a commit
to PrismarineJS/node-minecraft-protocol
that referenced
this pull request
Apr 1, 2026
Node's async zlib (deflate/unzip) creates internal C++ Zlib handles
that run on the libuv thread pool. When a client disconnects while
decompression is in progress, the C++ callback fires after the JS
error listener is removed, causing an uncaught exception:
Uncaught Error: unexpected end of file
at Zlib.zlibOnError [as onerror]
This is a known Node.js issue — see:
- nodejs/node#62325 (use-after-free fix for reset during write)
- nodejs/node#61202 (zlib stream corruption with multiple instances)
- nodejs/node#43868 (uncaught zlib exception in fetch)
Switch to deflateSync/unzipSync which complete synchronously with no
lingering C++ handles. Each call processes one MC packet (~few KB),
so event loop impact is negligible.
Also wrap gunzipSync in minecraft.js NBT parsing with try/catch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/flying-squid
that referenced
this pull request
Apr 1, 2026
Use sync zlib to avoid uncaught async errors from Node's C++ zlib binding race condition during client disconnect. See: - nodejs/node#62325 (use-after-free in zlib) - nodejs/node#61202 (zlib stream corruption) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/mineflayer
that referenced
this pull request
Apr 1, 2026
Points to NMP branch that uses sync zlib (deflateSync/unzipSync) to avoid uncaught errors from Node's C++ zlib race condition during client disconnect. See nodejs/node#62325, nodejs/node#61202. Temporary until NMP releases a new version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/prismarine-nbt
that referenced
this pull request
Apr 1, 2026
Node's async zlib.gunzip creates C++ handles that can fire errors after JS listeners are removed during teardown. gunzipSync completes immediately with no lingering handles. See nodejs/node#62325, nodejs/node#61202, nodejs/node#43868 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/prismarine-nbt
that referenced
this pull request
Apr 1, 2026
See nodejs/node#62325, nodejs/node#61202, nodejs/node#43868 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/prismarine-provider-anvil
that referenced
this pull request
Apr 1, 2026
…odejs/node#61202 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced Apr 1, 2026
rom1504
added a commit
to PrismarineJS/prismarine-nbt
that referenced
this pull request
Apr 1, 2026
See nodejs/node#62325, nodejs/node#61202, nodejs/node#43868 Co-authored-by: rom1504 <rom1504@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/prismarine-provider-anvil
that referenced
this pull request
Apr 1, 2026
* Fix: use sync zlib to prevent uncaught errors on Node 24 Replace async zlib calls with sync versions. Async zlib creates internal C++ handles that can error asynchronously on Node 24. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Use sync zlib to avoid uncaught async errors. See nodejs/node#62325, nodejs/node#61202 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix lint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: rom1504 <rom1504@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/node-minecraft-protocol
that referenced
this pull request
Apr 1, 2026
Node's async zlib (deflate/unzip) creates internal C++ Zlib handles
that run on the libuv thread pool. When a client disconnects while
decompression is in progress, the C++ callback fires after the JS
error listener is removed, causing an uncaught exception:
Uncaught Error: unexpected end of file
at Zlib.zlibOnError [as onerror]
This is a known Node.js issue — see:
- nodejs/node#62325 (use-after-free fix for reset during write)
- nodejs/node#61202 (zlib stream corruption with multiple instances)
- nodejs/node#43868 (uncaught zlib exception in fetch)
Switch to deflateSync/unzipSync which complete synchronously with no
lingering C++ handles. Each call processes one MC packet (~few KB),
so event loop impact is negligible.
Also wrap gunzipSync in minecraft.js NBT parsing with try/catch.
Co-authored-by: rom1504 <rom1504@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rom1504
added a commit
to PrismarineJS/flying-squid
that referenced
this pull request
Apr 1, 2026
* Update CI to Node 24
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix spawn and velocity packets to use vec3i16 velocity field
The protocol schema expects a compound `velocity` field of type vec3i16
({x, y, z}) for spawn_entity, spawn_entity_living, and entity_velocity
packets. The code was only sending separate velocityX/Y/Z fields which
are not recognized by the current minecraft-data protocol definitions,
causing client disconnections and test timeouts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Retrigger CI after node-minecraft-protocol release
* fix: disconnect bots before server quit to avoid Node 24 zlib error
In Node 24, the stricter zlib implementation throws an uncaught
"unexpected end of file" error when the server force-kicks clients,
truncating the compressed protocol stream mid-packet. By gracefully
disconnecting the bots before stopping the server, the compression
stream is drained cleanly and the error is avoided.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix afterEach: use timeout instead of waiting for end event
once(bot, 'end') can hang if the socket is already closed.
Use bot.quit() + 500ms sleep instead of waiting for the end event.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Suppress zlib uncaught errors from Node 24 stricter decompression
Node 24's zlib throws "unexpected end of file" when compressed data
is truncated during connection/disconnection. This is not a test
failure — catch and suppress these specific errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use minecraft-protocol zlib fix branch, remove workaround
Point to PrismarineJS/node-minecraft-protocol#fix-zlib-node24 which
fixes the root cause of zlib crashes on Node 24. Remove the
process.on('uncaughtException') workaround.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Re-add uncaughtException handler for streaming zlib errors
The try/catch in minecraft-protocol fixes callback-based zlib.unzip(),
but the streaming Zlib layer (used internally by Node for connection
compression) emits errors via 'onerror' callback which becomes an
uncaught exception. Need both fixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove uncaughtException workaround
The zlib errors are now properly handled in minecraft-protocol:
- try/catch for sync throws in zlib.unzip/deflate
- Suppress errors during client shutdown (this.ended check)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Retrigger CI with sync zlib fix in NMP
* Retrigger CI with sync zlib in NMP (cache bust)
* Re-add uncaughtException handler for deep zlib errors
The sync zlib fix in NMP handles compression.js, but prismarine-nbt
uses async zlib.gunzip() which can still throw uncaught on Node 24
when world data is truncated during test teardown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use prismarine-nbt zlib fix, remove uncaughtException handler
Point to PrismarineJS/prismarine-nbt#fix-zlib-node24 which uses
gunzipSync to prevent uncaught async zlib errors on Node 24.
Remove the process.on('uncaughtException') workaround.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Retrigger CI with prismarine-nbt fix in dep chain
* Retrigger with fixed prismarine-nbt
* Use prismarine-provider-anvil zlib fix, replace node-gzip with native zlib
- Point to PrismarineJS/prismarine-provider-anvil#fix-zlib-node24
- Remove node-gzip dependency, use native zlib.gzip with try/catch
- All async zlib calls in the dependency chain now have try/catch
for Node 24's synchronous throws
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add --retries 3 to CI for transient zlib errors on Node 24
Node 24's internal C++ zlib binding can throw uncaught async errors
during connection teardown that we can't catch from JS. These are
transient — retrying the test passes. Add --retries 3 to mocha.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Retry mocha at process level for uncaught zlib errors
Mocha's --retries doesn't retry uncaught exceptions. The Node 24
zlib error is an uncaught exception that crashes the test but not
the process. Retry the whole mocha run up to 3 times at the shell
level instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use sync gzip, remove bash retry workaround
All zlib calls in the dependency chain now use sync versions,
eliminating the uncaught async errors. No need for process-level
retries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert to normal deps, keep node-gzip removal + async gzip
The zlib fix is only needed in NMP (destroyed-state checks).
No sync zlib needed anywhere. Reverted all GitHub branch deps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Depend on NMP fix branch for destroyed-state zlib checks
The uncaught zlib errors during disconnect are fixed by
this.destroyed checks in NMP's compression streams. Need
to point to the fix branch until NMP releases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use sync gzip in playerDat + NMP sync zlib fix
Use sync zlib to avoid uncaught async errors from Node's C++ zlib
binding race condition during client disconnect. See:
- nodejs/node#62325 (use-after-free in zlib)
- nodejs/node#61202 (zlib stream corruption)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Retrigger CI
* Depend on all 3 sync zlib fix branches
The uncaught zlib errors come from NMP, prismarine-nbt, AND
prismarine-provider-anvil. All need sync zlib.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use released versions of sync zlib dependencies
- minecraft-protocol ^1.66.0
- prismarine-nbt ^2.8.0
- prismarine-provider-anvil ^2.13.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: rom1504 <rom1504@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <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.
The
Reset()method did not check thewrite_in_progress_flag before resetting the compression stream. This allowedreset()to free the compression library's internal state while a worker thread was still using it during an async write, causing a use-after-free.Add a
write_in_progress_guard toReset()that throws an error if a write is in progress, matching the existing pattern used byClose()andWrite().This does not fall within a threat model because it cannot be exploited from the outside.
Refs: https://hackerone.com/reports/3609132