Skip to content

zlib: fix use-after-free when reset() is called during write#62325

Merged
nodejs-github-bot merged 1 commit intonodejs:mainfrom
mcollina:fix-use-after-free-zlib
Mar 26, 2026
Merged

zlib: fix use-after-free when reset() is called during write#62325
nodejs-github-bot merged 1 commit intonodejs:mainfrom
mcollina:fix-use-after-free-zlib

Conversation

@mcollina
Copy link
Copy Markdown
Member

@mcollina mcollina commented Mar 18, 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().

This does not fall within a threat model because it cannot be exploited from the outside.

Refs: https://hackerone.com/reports/3609132

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
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. zlib Issues and PRs related to the zlib subsystem. labels Mar 18, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.68%. Comparing base (f08e2e0) to head (41f9937).
⚠️ Report is 50 commits behind head on main.

Files with missing lines Patch % Lines
src/node_zlib.cc 66.66% 0 Missing and 1 partial ⚠️
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     
Files with missing lines Coverage Δ
src/node_zlib.cc 77.94% <66.66%> (-0.40%) ⬇️

... and 27 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina mcollina added commit-queue Add this label to land a pull request using GitHub Actions. request-ci Add this label to start a Jenkins CI on a PR. and removed commit-queue Add this label to land a pull request using GitHub Actions. labels Mar 18, 2026
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 18, 2026
@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 20, 2026
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 26, 2026
@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

@mcollina mcollina added the commit-queue Add this label to land a pull request using GitHub Actions. label Mar 26, 2026
@nodejs-github-bot nodejs-github-bot removed the commit-queue Add this label to land a pull request using GitHub Actions. label Mar 26, 2026
@nodejs-github-bot nodejs-github-bot merged commit 53bcd11 into nodejs:main Mar 26, 2026
87 of 88 checks passed
@nodejs-github-bot
Copy link
Copy Markdown
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. zlib Issues and PRs related to the zlib subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants