Skip to content

node:zlib: accept ArrayBuffer and SharedArrayBuffer from another realm - #41101

Open
robobun wants to merge 2 commits into
mainfrom
robobun/56ed9fdb/zlib-cross-realm-arraybuffer
Open

robobun wants to merge 2 commits into
mainfrom
robobun/56ed9fdb/zlib-cross-realm-arraybuffer

Conversation

@robobun

@robobun robobun commented Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • node:zlib rejects an ArrayBuffer or SharedArrayBuffer that was created in another realm (vm.runInContext). Every convenience method (gzipSync, gzip, deflateSync, brotliCompressSync, zstdCompressSync, ...) and the dictionary option throw ERR_INVALID_ARG_TYPE. The message contradicts itself: The "buffer" argument must be of type string, Buffer, TypedArray, DataView, or ArrayBuffer. Received an instance of ArrayBuffer. Node 26.3.0 accepts the same input.
  • The cause is the local check in src/js/node/zlib.ts:21: b instanceof ArrayBuffer || b instanceof SharedArrayBuffer. instanceof walks the prototype chain, and a buffer from another realm has that realm's ArrayBuffer.prototype. TypedArray and DataView inputs were not affected because ArrayBuffer.isView reads the internal type.

Fix

  • Take isArrayBufferView and isAnyArrayBuffer from node:util/types. Both are native and check the cell type (JSArrayBuffer, typed array or DataView), so the realm does not matter. This is what node's lib/zlib.js does, and what node:crypto and the internal stream modules in bun already do.
  • Verified: test/js/node/zlib/zlib.test.js (new ArrayBuffer from another realm block, 15 of 17 cases fail on bun 1.4.1). Also the rest of test/js/node/zlib/ and the vendored test-zlib-convenience-methods.js, test-zlib-dictionary.js, test-zlib-brotli-dictionary.js, test-zlib-zstd-dictionary.js, test-zlib-not-string-or-buffer.js, test-zlib-invalid-input.js.

Background

  • A realm is one set of JavaScript globals. vm.createContext() creates a new realm with its own ArrayBuffer, Object and so on. A value created there and passed to the main realm has a different [[Prototype]], so instanceof against a main-realm constructor is false.
  • util.types.isAnyArrayBuffer(value) is realm-independent. Bun implements it in src/jsc/modules/NodeUtilTypesModule.cpp as dynamicDowncast<JSArrayBuffer>.
Notes

Repro on bun 1.4.1 (node 26.3.0 prints the decompressed bytes for every line):

const vm = require("node:vm");
const zlib = require("node:zlib");
const ab = vm.runInContext("new ArrayBuffer(4)", vm.createContext({}));
new Uint8Array(ab).set([1, 2, 3, 4]);
zlib.gunzipSync(zlib.gzipSync(ab));            // ERR_INVALID_ARG_TYPE
zlib.deflateSync("x", { dictionary: ab });     // ERR_INVALID_ARG_TYPE (options.dictionary)
zlib.gzip(ab, cb);                             // writable rejects the raw ArrayBuffer as a chunk

test/js/node/zlib/leak.test.ts times out in its beforeAll on this debug ASAN build with and without this change (10,000 deflateSync calls in one hook). It is not related to this diff.


[human-review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 15 failed, 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/zlib/zlib.test.js
bun test v1.4.1 (a6c4cc276)

test/js/node/zlib/zlib.test.js:
(pass) prototype and name and constructor > Gzip > Gzip.prototype should be instanceof Gzip.__proto__ [3.40ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.constructor should be Gzip [2.32ms]
(pass) prototype and name and constructor > Gzip > Gzip.name should be Gzip [1.58ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.__proto__.constructor.name should be Zlib [2.84ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype should be instanceof Gunzip.__proto__ [0.65ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.constructor should be Gunzip [0.41ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.name should be Gunzip [0.41ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.__proto__.constructor.name should be Zlib [0.44ms]
(pass) prototype and name and constructor > Deflate > Deflate.prototype should be instanceof Deflate.__proto
... (truncated)

release without fix: 15 failed, 2 skipped
bun test v1.4.1-canary.1 (a6c4cc276)

test/js/node/zlib/zlib.test.js:
(pass) prototype and name and constructor > Gzip > Gzip.prototype should be instanceof Gzip.__proto__ [0.03ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.constructor should be Gzip [0.01ms]
(pass) prototype and name and constructor > Gzip > Gzip.name should be Gzip
(pass) prototype and name and constructor > Gzip > Gzip.prototype.__proto__.constructor.name should be Zlib [0.01ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype should be instanceof Gunzip.__proto__
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.constructor should be Gunzip
(pass) prototype and name and constructor > Gunzip > Gunzip.name should be Gunzip
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.__proto__.constructor.name should be Zlib
(pass) prototype and name and constructor > Deflate > Deflate.prototype should be instanceof Deflate.__proto__
(pass) prototype and name and constructor > Deflate > Deflate.prototype.constructor should be Deflate
(pass) prototype and name and constructor > Deflate > Deflate.name should be Deflate
(pass) pr
... (truncated)
passes on PR (with fix)
ASAN with fix: 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/zlib/zlib.test.js
bun test v1.4.1 (a6c4cc276)

test/js/node/zlib/zlib.test.js:
(pass) prototype and name and constructor > Gzip > Gzip.prototype should be instanceof Gzip.__proto__ [2.10ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.constructor should be Gzip [1.46ms]
(pass) prototype and name and constructor > Gzip > Gzip.name should be Gzip [1.05ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.__proto__.constructor.name should be Zlib [1.69ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype should be instanceof Gunzip.__proto__ [0.41ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.constructor should be Gunzip [0.25ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.name should be Gunzip [0.24ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.__proto__.constructor.name should be Zlib [0.32ms]
(pass) prototype and name and constructor > Deflate > Deflate.prototype should be instanceof Deflate.__proto
... (truncated)

release with fix: 2 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 714ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/28] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 244 extern-C blocks audited
[2/28] gen cpp.rs (cppbind)
[3/28] gen JS modules (bundle-modules)
Preprocess modules (12161ms)
Bundle modules (61ms)
Postprocesss modules (26ms)
Bundle Functions (569ms)
Generate Code (33ms)

[12.86s] Bundled "src/js" for production
  2594 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[3/23] cargo bun_runtime → libbun_runtime.a
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src
... (truncated)
diff hotspot
src/js/node/zlib.ts            |  3 +-
 test/js/node/zlib/zlib.test.js | 66 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 67 insertions(+), 2 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                            reads  edits  tests
src/js/node/zlib.ts                 2      1      6
test/js/node/zlib/zlib.test.js      2      2      5

zlib.ts checked buffers with a local `instanceof ArrayBuffer ||
instanceof SharedArrayBuffer`. That check is false for a buffer created
in a node:vm context, so the sync and async convenience methods and the
`dictionary` option rejected it with ERR_INVALID_ARG_TYPE ("Received an
instance of ArrayBuffer"). Node uses util.types.isAnyArrayBuffer, which
checks the internal class and not the realm's prototype.

Take isArrayBufferView and isAnyArrayBuffer from node:util/types, like
node:crypto and the internal stream modules already do.
@robobun

robobun commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on bun 1.4.1 with an ArrayBuffer from vm.runInContext: zlib.gzipSync(ab), zlib.gzip(ab, cb), zlib.deflateSync("x", { dictionary: ab }) and the brotli/zstd equivalents all throw ERR_INVALID_ARG_TYPE. Node 26.3.0 accepts the same input.

Verified with a debug build: test/js/node/zlib/zlib.test.js (405 pass, 15 of the 17 new cases fail on 1.4.1), the rest of test/js/node/zlib/, and the vendored test-zlib-*dictionary*.js, test-zlib-convenience-methods.js, test-zlib-not-string-or-buffer.js, test-zlib-invalid-input.js.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 414baf58-12c7-46d4-bdd1-094097220825

📥 Commits

Reviewing files that changed from the base of the PR and between 561641b and c094249.

📒 Files selected for processing (2)
  • src/js/node/zlib.ts
  • test/js/node/zlib/zlib.test.js

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

Changes

Cross-realm zlib buffer handling

Layer / File(s) Summary
Cross-realm buffer predicates
src/js/node/zlib.ts
zlib uses node:util/types predicates for ArrayBuffer views and ArrayBuffer values.
Cross-realm zlib validation
test/js/node/zlib/zlib.test.js
Tests cover cross-realm ArrayBuffer and SharedArrayBuffer values across synchronous and asynchronous compression and decompression, including dictionaries and invalid inputs.

Suggested reviewers: cirospaciari, jarred-sumner

Merge Risk: ⚪ Minimal · up to c0942

This localized change broadens zlib support for cross-realm ArrayBuffer inputs and adds targeted coverage; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: cross-realm ArrayBuffer and SharedArrayBuffer support in node:zlib.
Description check ✅ Passed The description explains the problem, root cause, fix, affected APIs, tests, and verification results. It does not use the template headings exactly, but it provides the required information and is co…
Full details: Description check

Explanation

The description explains the problem, root cause, fix, affected APIs, tests, and verification results. It does not use the template headings exactly, but it provides the required information and is complete.


Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

The automated review raised no findings on c094249. No open review threads. CI build 109027 is in progress; the diff is a two-line change in zlib.ts plus tests, so no further changes are planned unless CI shows a failure in the zlib tests.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — swaps instanceof ArrayBuffer/SharedArrayBuffer for the realm-independent util.types brand checks, matching Node's own lib/zlib.js and 27 other built-in modules in this repo.

What was reviewed:

  • All 12 call sites of isArrayBufferView/isAnyArrayBuffer in zlib.ts — the new tests exercise every path (sync/async convenience, Zlib and Brotli dictionary validation).
  • isArrayBufferView: ArrayBuffer.isView was already realm-safe, so this half is a no-op behavior-wise; isAnyArrayBuffer is the actual fix.
  • Negative case preserved: cross-realm plain object still throws ERR_INVALID_ARG_TYPE.
Extended reasoning...

Overview

This PR touches two files: a two-line change in src/js/node/zlib.ts replacing local isArrayBufferView (was ArrayBuffer.isView) and isAnyArrayBuffer (was instanceof ArrayBuffer || instanceof SharedArrayBuffer) with the same-named functions from require("node:util/types"), and a new describe block in test/js/node/zlib/zlib.test.js that verifies cross-realm ArrayBuffer/SharedArrayBuffer inputs are accepted by every convenience method and the dictionary option.

Security risks

None. The change tightens type detection to a native brand check (JSArrayBuffer cell type via dynamicDowncast) rather than a prototype-chain walk. This is strictly less spoofable than instanceof, and it does not widen the accepted input space beyond what Node accepts — the negative test confirms non-buffer objects from the vm realm still throw ERR_INVALID_ARG_TYPE.

Level of scrutiny

Low. The fix is a well-understood Node-compat pattern: instanceof fails cross-realm, util.types.is* does not. The exact same import is already used by 27 other built-in modules in src/js/ (crypto, fs, tls, streams, etc.), so this follows the established in-tree convention that REVIEW.md calls for. Node's own lib/zlib.js uses internal/util/types for the same two functions, so this brings Bun's zlib in line with the reference implementation.

Other factors

Test coverage is thorough for the size of the fix: it covers all five sync compress/decompress pairs, four async pairs, both ArrayBuffer and SharedArrayBuffer, both dictionary code paths (Zlib at line 562 and Brotli at line 720), and the rejection path. The precondition assertion (instanceof ArrayBuffer is false) ensures the test would fail on the old code for the right reason. No CODEOWNERS entries cover these paths, no prior reviewer objections exist in the timeline, and the bug hunt exited on dry_streak with no findings.

@robobun

robobun commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Both automated reviews are in with no requested changes. No open review threads. CI build 109027 is at 27 of 181 jobs passed, the rest are running. I will report back when it finishes.

@robobun

robobun commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Nothing new to address from the updated automated summary (no findings). Build 109027 finished with 180 of 181 jobs green. The one failed job ran test/cli/run/require-cache.test.ts on the x64 ASAN lane, which times out on main as well (3 of the last 8 main builds) and has no relation to this change. Build 109035 is the re-run and is at 104 of 181 passed so far.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants