Skip to content

Deduplicate zlib/brotli/zstd binding helpers - #31996

Open
alii wants to merge 18 commits into
mainfrom
claude/split/zlib
Open

Deduplicate zlib/brotli/zstd binding helpers#31996
alii wants to merge 18 commits into
mainfrom
claude/split/zlib

Conversation

@alii

@alii alii commented Jun 8, 2026

Copy link
Copy Markdown
Member

What this does

Deduplicates the node zlib native bindings and removes dead FFI surface. Net -93 lines against current main.

Binding helpers (node_zlib_binding.rs, used by NativeZlib/NativeBrotli/NativeZstd): the constructor mode check, the writeResult/writeState check, and the Uint32Array params check each existed three times; write and writeSync each carried their own copy of the 7-argument parsing. They become validate_mode, validate_write_result_array, validate_uint32_array, and parse_write_args. The extracted code is order-for-order and message-for-message identical to the inlined originals, which the new tests pin.

parse_write_args is generic over the stream type so it calls the per-codec flush_value_is_valid added by #34427, and takes a reject_resizable flag because the two copies had diverged on main: the async write rejects non-shared resizable ArrayBuffers (it captures the buffer for the threadpool, #36555) while writeSync does not. The flag keeps that difference explicit instead of hiding it in two parsers.

zlib crate: map_init_return_code and regrow_output_tail replace repeated init/regrow blocks in the two ArrayList types. bun_io was an unused dependency.

zlib_sys: posix.rs and win32.rs only re-exported shared types plus extern declarations nothing referenced; src/zlib/lib.rs declares the externs it actually calls itself. Both files go and the cfg-selected re-export in mod internal becomes a plain shared import. Both paths are added to the source-lint gone[] list, which checks the committed tree, so a stray index cannot quietly bring them back (that happened once during this PR's history).

Two small hardenings found while writing the tests, still unique to this PR:

  • NativeZstd::init validates initParamsArray before s.init() allocates the context. Previously a type error there threw after the context existed, leaving a live encoder configured with default parameters behind the thrown error; a follow-up write on that handle compressed data. The new test fails on main and passes here.
  • ZlibReaderArrayList/ZlibCompressorArrayList reject inputs longer than zlib's 32-bit avail_in instead of truncating to a prefix (and, on Windows where uLong is 32-bit, panicking at the deflateBound cast). On the output side, avail_out is clamped to 32 bits at the two live sites: reserve_expand_tail hands back the Vec's whole slack, and a buffer with exactly 4 GiB spare used to truncate to 0 and fail a valid compress with ShortRead. Both need >4 GiB buffers to exercise, so neither has a CI test.

History

Opened as a split of #31912. Earlier revisions also fixed crashes when finalizing or closing a never-initialized handle; #32762 landed the complete fix for that class on main and this PR now uses those versions. Main's dead-code sweeps also removed the unused ZlibReader type this PR originally deleted. What remains above is everything main has not absorbed, re-verified against main at the latest merge.

Verification

  • test/js/node/zlib/zlib-handle-bounds-check.test.ts: contract tests for the constructors, init, write, writeSync (error class, code, and message), the zstd half-init regression test, and a lifecycle test covering failed-validation init() followed by close() and GC across all three classes. 48/48 pass with this PR; with src/ reverted to main exactly the zstd test fails.
  • All of test/js/node/zlib/* (including zlib.test.js, 387 pass), the 61 upstream test-zlib-* node tests, test/js/bun/util/zstd.test.ts, and the fetch decompression regression tests pass on a debug build of the merged head.
  • cargo check and cargo clippy clean for bun_zlib_sys, bun_zlib, bun_runtime, natively and for x86_64-pc-windows-msvc (relevant because win32.rs is deleted).

[review] gate passed · iteration 22 · 12 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts test/js/node/zlib/zlib-handle-bounds-check.test.ts
bun test v1.4.0 (87a9140c0)

test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts:
(pass) dead FFI declarations (sys crates) do not reappear [55.20ms]
(pass) dead Rust symbols (bun_core, jsc, test_runner) do not reappear [15.13ms]
(pass) orphaned files stay deleted [648.02ms]
(pass) dead JS/codegen helpers do not reappear [68.88ms]
(pass) stale commented-out C++ blocks stay deleted [29.86ms]

test/js/node/zlib/zlib-handle-bounds-check.test.ts:
(pass) zlib native handle bounds checking > writeSync rejects in_len exceeding input buffer [14.19ms]
(pass) zlib native handle bounds checking > writeSync rejects out_len exceeding output buffer [7.13ms]
(pass) zlib native handle bounds checking > writeSync rejects in_off + in_len exceeding input buffer [6.50ms]
(pass) zlib native handle bounds checking > writeSync rejects out_off + out_len exceeding output buffer [5.94ms]
(pass) zlib native handle bounds checking > w
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (4562fd945)

test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts:
(pass) dead FFI declarations (sys crates) do not reappear [3.55ms]
(pass) dead Rust symbols (bun_core, jsc, test_runner) do not reappear [0.64ms]
(pass) orphaned files stay deleted [36.69ms]
(pass) dead JS/codegen helpers do not reappear [33.53ms]
(pass) stale commented-out C++ blocks stay deleted [15.37ms]

test/js/node/zlib/zlib-handle-bounds-check.test.ts:
(pass) zlib native handle bounds checking > writeSync rejects in_len exceeding input buffer [0.41ms]
(pass) zlib native handle bounds checking > writeSync rejects out_len exceeding output buffer [0.29ms]
(pass) zlib native handle bounds checking > writeSync rejects in_off + in_len exceeding input buffer [0.18ms]
(pass) zlib native handle bounds checking > writeSync rejects out_off + out_len exceeding output buffer [0.25ms]
(pass) zlib native handle bounds checking > writeSync allows valid bounds [0.27ms]
(pass) zlib native handle bounds checking > writeSync allows valid offset + length within bounds [0.23ms]
(pass) zlib native handle bounds checking > writeSync allows null input (flush only) [0.19ms]
(pass) 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts test/js/node/zlib/zlib-handle-bounds-check.test.ts
bun test v1.4.0 (87a9140c0)

test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts:
(pass) dead FFI declarations (sys crates) do not reappear [57.17ms]
(pass) dead Rust symbols (bun_core, jsc, test_runner) do not reappear [15.85ms]
(pass) orphaned files stay deleted [954.50ms]
(pass) dead JS/codegen helpers do not reappear [94.93ms]
(pass) stale commented-out C++ blocks stay deleted [47.60ms]

test/js/node/zlib/zlib-handle-bounds-check.test.ts:
(pass) zlib native handle bounds checking > writeSync rejects in_len exceeding input buffer [15.08ms]
(pass) zlib native handle bounds checking > writeSync rejects out_len exceeding output buffer [7.79ms]
(pass) zlib native handle bounds checking > writeSync rejects in_off + in_len exceeding input buffer [6.84ms]
(pass) zlib native handle bounds checking > writeSync rejects out_off + out_len exceeding output buffer [7.46ms]
(pass) zlib native handle bounds checking > w
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1166ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 93 exports (host=3, lazy=10, generic=80, rust=0); 239 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[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_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli 
... (truncated)
diff hotspot
Cargo.lock                                         |   1 -
 src/runtime/node/node_zlib_binding.rs              | 452 +++++++++++----------
 src/runtime/node/zlib/NativeBrotli.rs              |  82 +---
 src/runtime/node/zlib/NativeZlib.rs                |  63 +--
 src/runtime/node/zlib/NativeZstd.rs                |  81 +---
 src/zlib/Cargo.toml                                |   1 -
 src/zlib/lib.rs                                    | 138 +++----
 src/zlib_sys/lib.rs                                |   2 -
 src/zlib_sys/posix.rs                              |  43 --
 src/zlib_sys/win32.rs                              | 129 ------
 .../dead-symbols-pub-exports-sweep.test.ts         |   7 +-
 test/js/node/zlib/zlib-handle-bounds-check.test.ts | 234 +++++++++++
 12 files changed, 578 insertions(+), 655 deletions(-)

gate history · 5 passed · 0 rejected · iteration 22

evidence per changed file
file                                                      reads  edits  tests
Cargo.lock                                                    0      0     12
src/runtime/node/node_zlib_binding.rs                         5      5     12
src/runtime/node/zlib/NativeBrotli.rs                         2      3     12
src/runtime/node/zlib/NativeZlib.rs                           2      4     12
src/runtime/node/zlib/NativeZstd.rs                           5      7     12
src/zlib/Cargo.toml                                           0      0     12
src/zlib/lib.rs                                              11     11     14
src/zlib_sys/lib.rs                                           0      0     12
src/zlib_sys/posix.rs                                         0      0     12
src/zlib_sys/win32.rs                                         0      0     12
…nal/source-lints/dead-symbols-pub-exports-sweep.test.ts      1      2      0
test/js/node/zlib/zlib-handle-bounds-check.test.ts            6      8     12

@robobun

robobun commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator
Updated 7:16 PM PT - Aug 10th, 2026

@robobun, your commit 87a9140c0f33a3da8a64f170a0959caf9069be96 passed in Build #91826! 🎉


🧪   To try this PR locally:

bunx bun-pr 31996

That installs a local version of the PR into your bun-31996 executable, so you can run:

bun-31996 --bun

@alii
alii marked this pull request as ready for review June 9, 2026 20:18
@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@alii
alii force-pushed the claude/split/zlib branch from b38f5fd to 7ac9c1f Compare June 9, 2026 20:19
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR centralizes zlib argument validation, updates native Brotli, zlib, and Zstd streams, unifies zlib initialization and output growth, narrows FFI exports, removes an unused dependency, and adds validation and lifecycle tests.

Changes

Zlib validation and compression stream unification

Layer / File(s) Summary
Shared validators and write parsing
src/runtime/node/node_zlib_binding.rs
Adds shared mode, typed-array, write-result, and write-argument validation. Refactors write and writeSync to use parsed arguments and validated buffer ranges.
Native stream validation and teardown
src/runtime/node/zlib/NativeBrotli.rs, src/runtime/node/zlib/NativeZlib.rs, src/runtime/node/zlib/NativeZstd.rs
Native streams use shared validators and task callbacks. Zstd validates initialization parameters. Close paths avoid teardown when stream state is absent.
Platform-independent zlib bindings
src/zlib_sys/lib.rs, src/zlib_sys/posix.rs, src/zlib_sys/win32.rs, src/zlib/lib.rs
Removes public platform-specific FFI modules and uses shared bindings internally.
Unified zlib initialization and output growth
src/zlib/lib.rs
Checks uInt input bounds, shares initialization error mapping, centralizes output growth, and uses deflateBound for compressor capacity.
Validation tests and crate cleanup
test/js/node/zlib/zlib-handle-bounds-check.test.ts, src/zlib/Cargo.toml
Adds constructor, initialization, write, bounds, and lifecycle tests. Removes the unused bun_io dependency.

Possibly related PRs

  • oven-sh/bun#36555: Both changes update async node:zlib write-buffer validation and related bounds tests.
  • oven-sh/bun#35880: Both changes modify zlib crate dependency or export configuration.

Suggested reviewers: robobun, jarred-sumner, cirospaciari

🚥 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.
Description check ✅ Passed The description clearly explains the changes, objectives, regressions, and verification results, although its headings differ from the template.
Title check ✅ Passed The title clearly and concisely describes the main change: shared helper deduplication across zlib, Brotli, and Zstd bindings.

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

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Adopted, brought up to date with main, and mergeable at 87a9140. Scope: three-way dedup of the binding validation helpers, a shared write/writeSync argument parser, removal of the dead zlib_sys posix/win32 split (now also guarded by the orphaned-files source lint), zstd validate-before-allocate (regression test fails on main), and 32-bit guards on both avail_in and avail_out in the zlib crate. All review threads resolved. Verified on this head: handle tests 48/48, zlib.test.js, zstd.test.ts, fetch decompression regressions, source lints 75/75, cargo check + clippy including the Windows target. Buildkite build 91826 running.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/node/zlib/NativeZstd.rs (1)

135-164: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate initParamsArray before initializing Zstd state.

validate_uint32_array(...) can still throw after writeState/callback caching and after s.init() allocates a CCtx/DCtx. That leaves the instance half-initialized, and a later retry can leak the first context because Context::init() just replaces self.state.

Suggested fix
             let init_params_array_value = arguments[0];
             let pledged_src_size_value = arguments[1];
             let write_state_value = arguments[2];
             let process_callback_value = arguments[3];

             validate_write_result_array(global, write_state_value, "writeState")?;
+            let mut params_ =
+                validate_uint32_array(global, init_params_array_value, "initParamsArray")?;
             js::write_result_set_cached(this_value, global, write_state_value);

             let write_js_callback =
                 validators::validate_function(global, "processCallback", process_callback_value)?;
@@
             if err.is_error() {
                 CompressionStream::<Self>::emit_error(self, global, this_value, err);
                 return Ok(JSValue::FALSE);
             }

-            let mut params_ =
-                validate_uint32_array(global, init_params_array_value, "initParamsArray")?;
             for (i, &x) in params_.as_u32().iter().enumerate() {

As per coding guidelines, untrusted input must be validated before any processing, allocation, or side effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/node/zlib/NativeZstd.rs` around lines 135 - 164, The code
validates initParamsArray after caching writeState/callback and after calling
self.stream.with_mut(|s| s.init(...)), which can allocate and leave the instance
half-initialized if validate_uint32_array throws; move the
validate_uint32_array(global, init_params_array_value, "initParamsArray") call
to before any side-effects or allocations (i.e., before calling
self.stream.with_mut(|s| s.init(pledged_src_size)) and preferably before
js::write_callback_set_cached/js::write_result_set_cached) so all untrusted
input is validated first; update references to init_params_array_value and
params_ accordingly and keep CompressionStream::emit_error handling for init
failures.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/node/node_zlib_binding.rs`:
- Around line 402-467: In parse_write_args, don't cast JS values to u32 using
the shim jsv_to_u32 before validating them; instead call the existing
validate_uint32 on the flush, in_off, in_len, out_off, and out_len argument
values (the same argument indices used around jsv_to_u32) to ensure they are
finite unsigned 32-bit integers, then convert to u32 for further checks; keep
the existing flush_value_is_valid(flush) and the buffer bound checks for
in_buf.byte_len and out_buf.byte_len but use the validated/converted values
(refer to the parse_write_args function, the jsv_to_u32 shim, and
validate_uint32 helper).

In `@src/zlib/lib.rs`:
- Around line 773-778: The call in
ZlibCompressorArrayList::init_with_list_allocator uses
uLong::try_from(input.len()).expect("int cast") before calling deflateBound,
which can panic on platforms where uLong is 32-bit; replace the expect with a
checked conversion that returns Err(ZlibError::InvalidArgument) (or the
appropriate existing ZlibError variant) when input.len() does not fit into
zlib's uLong, and only call deflateBound with the safely converted value
(reference: deflateBound, zlib_reader.zlib).

---

Outside diff comments:
In `@src/runtime/node/zlib/NativeZstd.rs`:
- Around line 135-164: The code validates initParamsArray after caching
writeState/callback and after calling self.stream.with_mut(|s| s.init(...)),
which can allocate and leave the instance half-initialized if
validate_uint32_array throws; move the validate_uint32_array(global,
init_params_array_value, "initParamsArray") call to before any side-effects or
allocations (i.e., before calling self.stream.with_mut(|s|
s.init(pledged_src_size)) and preferably before
js::write_callback_set_cached/js::write_result_set_cached) so all untrusted
input is validated first; update references to init_params_array_value and
params_ accordingly and keep CompressionStream::emit_error handling for init
failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6c206be3-e6b4-44e6-a30e-82332eb40519

📥 Commits

Reviewing files that changed from the base of the PR and between 09703da and 7614467.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • src/runtime/node/node_zlib_binding.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • src/runtime/node/zlib/NativeZlib.rs
  • src/runtime/node/zlib/NativeZstd.rs
  • src/zlib/Cargo.toml
  • src/zlib/lib.rs
  • src/zlib_sys/lib.rs
  • src/zlib_sys/posix.rs
  • src/zlib_sys/win32.rs
💤 Files with no reviewable changes (4)
  • src/zlib_sys/lib.rs
  • src/zlib_sys/posix.rs
  • src/zlib/Cargo.toml
  • src/zlib_sys/win32.rs

Comment thread src/runtime/node/node_zlib_binding.rs
Comment thread src/zlib/lib.rs Outdated
robobun and others added 2 commits June 10, 2026 03:40
…test binding validation

Context::close() for NativeZlib/NativeBrotli/NativeZstd now tolerates a
handle whose init() never ran (failed argument validation, or init was
never called): Brotli unwrapped a None state pointer, Zstd passed NULL to
ZSTD_*Ctx_reset, and Zlib tripped its debug assert on deflateEnd's
StreamError. GC finalization of such a handle crashed the process.

ZlibReaderArrayList/ZlibCompressorArrayList init now rejects inputs whose
length does not fit zlib's 32-bit avail_in instead of silently truncating
(and panicking on the Windows deflateBound cast, where uLong is 32-bit).

Adds native-handle tests pinning the argument validation contract of the
consolidated helpers and the never-initialized lifecycle.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/node/zlib/NativeZstd.rs (1)

157-165: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate initParamsArray before creating the ZSTD context.

self.stream.with_mut(|s| s.init(pledged_src_size)) runs before validate_uint32_array(...). If initParamsArray is the wrong typed array, this method throws after ZSTD_create{C,D}Ctx has already succeeded, so the handle stays partially initialized until an explicit close() or later GC finalization.

♻️ Suggested fix
-            let err = self.stream.with_mut(|s| s.init(pledged_src_size));
+            let mut params_ =
+                validate_uint32_array(global, init_params_array_value, "initParamsArray")?;
+
+            let err = self.stream.with_mut(|s| s.init(pledged_src_size));
             if err.is_error() {
                 CompressionStream::<Self>::emit_error(self, global, this_value, err);
                 return Ok(JSValue::FALSE);
             }
-
-            let mut params_ =
-                validate_uint32_array(global, init_params_array_value, "initParamsArray")?;
             for (i, &x) in params_.as_u32().iter().enumerate() {

As per coding guidelines, "Validate untrusted input BEFORE any processing, allocation, or side effect."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/node/zlib/NativeZstd.rs` around lines 157 - 165, Validate the
incoming initParamsArray before allocating or initializing the ZSTD context:
call validate_uint32_array(global, init_params_array_value, "initParamsArray")
and handle/return its Err result before invoking self.stream.with_mut(|s|
s.init(pledged_src_size)) (and before any ZSTD_create*Ctx side-effects); then
use the validated params_ in the later loop. This ensures the ZSTD context
(created via s.init / ZSTD_createCCtx/ZSTD_createDCtx) is only created after
untrusted input is checked and avoids leaving a partially initialized handle
when validation fails.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/zlib/zlib-handle-bounds-check.test.ts`:
- Around line 286-287: Add an assertion that stderr is empty before checking
stdout/exitCode: after the destructuring of [stdout, stderr, exitCode] (the
Promise.all call), insert expect(stderr.trim()).toBe("") and then assert stdout
and exitCode as currently done (or replace the single object equality with
ordered assertions: expect(stdout.trim()).toBe("survived");
expect(exitCode).toBe(0)). This ensures any unexpected stderr output is caught;
reference the variables stdout, stderr, exitCode from the test's Promise.all
destructuring.

---

Outside diff comments:
In `@src/runtime/node/zlib/NativeZstd.rs`:
- Around line 157-165: Validate the incoming initParamsArray before allocating
or initializing the ZSTD context: call validate_uint32_array(global,
init_params_array_value, "initParamsArray") and handle/return its Err result
before invoking self.stream.with_mut(|s| s.init(pledged_src_size)) (and before
any ZSTD_create*Ctx side-effects); then use the validated params_ in the later
loop. This ensures the ZSTD context (created via s.init /
ZSTD_createCCtx/ZSTD_createDCtx) is only created after untrusted input is
checked and avoids leaving a partially initialized handle when validation fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a98753f7-bb5e-4505-b91d-a7c03867b1ac

📥 Commits

Reviewing files that changed from the base of the PR and between 7614467 and 54a06e2.

📒 Files selected for processing (5)
  • src/runtime/node/zlib/NativeBrotli.rs
  • src/runtime/node/zlib/NativeZlib.rs
  • src/runtime/node/zlib/NativeZstd.rs
  • src/zlib/lib.rs
  • test/js/node/zlib/zlib-handle-bounds-check.test.ts

Comment thread test/js/node/zlib/zlib-handle-bounds-check.test.ts Outdated
Comment thread test/js/node/zlib/zlib-handle-bounds-check.test.ts Outdated
… subprocess stderr

Moves the initParamsArray type check ahead of ZSTD context creation in
NativeZstd::init so a type error cannot leave a half-initialized handle,
and folds stderr into the lifecycle test's combined assertion so a crash
regression shows the panic text in the failure diff.
Comment thread src/runtime/node/zlib/NativeZstd.rs

@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.

Both earlier nits are addressed and the bug-hunting pass is clean; deferring only because this is a sizeable refactor of unsafe native compression bindings (zlib/brotli/zstd close/init paths) that's worth a human look before merge.

Extended reasoning...

Overview

Consolidates duplicated argument-validation and write-arg-parsing helpers across NativeZlib/NativeBrotli/NativeZstd, removes the dead zlib_sys posix/win32 split and the unused ZlibReader (+bun_io dep), adds map_init_return_code/regrow_output_tail helpers in src/zlib/lib.rs, and fixes pre-existing crashes when close()/GC-finalize runs on a handle whose init() never succeeded. New tests pin the validation contract and the lifecycle fix.

Since my last review

Both prior inline comments are resolved: the lifecycle subprocess test now asserts {stdout, stderr, exitCode} together, and the PR description's final paragraph was rewritten to accurately describe the c67f705 initParamsArray reorder. The only new commit is the main merge (3cafa81). No bugs surfaced in the current bug-hunting pass.

Security risks

None identified. The changes tighten validation (reject >4 GiB inputs instead of truncating, guard null-state close()) rather than relaxing it. No auth/crypto/permission surface.

Level of scrutiny

Medium-high. This is production runtime code with unsafe FFI (zlib/brotli/zstd state lifecycle, raw buffer pointers into JS-owned ArrayBuffers), and the diff includes behavioral changes beyond pure deduplication: close() null-state guards in three backends, the zstd initParamsArray validation reorder, and the 32-bit avail_in rejection. The consolidation itself reads as message-for-message faithful and the new tests cover it well, but the scope (~11 files, several hundred net lines across unsafe native bindings) puts it outside what I'd auto-approve.

Other factors

robobun adopted/verified on a debug build with the full zlib suite + Windows cross-target check; all CodeRabbit threads are resolved. No outstanding reviewer asks.

# Conflicts:
#	src/runtime/node/zlib/NativeBrotli.rs
#	src/runtime/node/zlib/NativeZstd.rs
@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Merged main and resolved the conflicts. Two of the three were real overlaps: #32762 landed the same never-initialized close() guards this PR carried, so I took main's versions and dropped the duplicates here; #32637 added crc32_bytes and the DeflateEncoder/InflateDecoder wrappers next to code this PR deletes, and those are preserved. Details are in the updated PR description. All 39 tests in the zlib handle suite (this PR's plus #32762's) and the rest of the compression suites pass on the merged head, and the Windows cross-target check compiles clean.

robobun added 2 commits June 28, 2026 12:28
…ed handle

After init() throws on an invalid initParamsArray, a write on the handle
must move no bytes. Validating after ZSTD context creation left a usable
encoder configured with default parameters behind the thrown error.
@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Merged main again (5310acc). The conflict was in src/zlib/lib.rs: #33909 switched error machinery to per-crate thiserror enums (dropped the named_error_set macro call accordingly) and updated the ZlibReader impl this PR deletes; the deletion stays. Verified: all 40 zlib handle tests and the rest of the compression suites pass on the merged head, the gate's fail-before test still fails on main's src and passes here, and the Windows cross-target check compiles clean.

@alii

alii commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@robobun get this mergeable — or close it if the changes are already covered on main / too stale to salvage.

@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Checked against current main before deciding: none of this is covered there yet. Main still has the three copies of the constructor mode check and the writeResult checks, both copies of the write/writeSync argument parsing, the zlib_sys posix/win32 split, the zstd init ordering, and the truncating input-length casts. So it is worth salvaging rather than closing. Bringing it up to date now: four conflicts, mostly from #34427 (dictionaries) and #36555 (growable SAB) adding new validation to the write path that the consolidated parser has to absorb. Will post when it is green.

# Conflicts:
#	src/runtime/node/node_zlib_binding.rs
#	src/runtime/node/zlib/NativeZstd.rs
#	src/zlib/lib.rs
#	src/zlib_sys/win32.rs
@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Up to date with main as of 926eba0 and mergeable again; CI is running on build 91663.

What the merge changed: main had moved flush validation onto the per-codec trait (#34427) and added the resizable-ArrayBuffer rejection to the async write path only (#36555), so parse_write_args is now generic over the stream type and takes a reject_resizable flag that write passes as true and writeSync as false, keeping that deliberate difference visible. Main's dead-code sweeps had already removed ZlibReader, so that part of the PR dropped out; the rest (the three-way helper dedup, the write/writeSync parser, the zlib_sys posix/win32 removal, the zstd init ordering, and the input-length guards) is still not on main. Description rewritten to describe only what remains.

Verified on the merged head: 48/48 in the handle test file (the zstd test still fails with src reverted to main), zlib.test.js 387 pass, all 61 upstream test-zlib-* files, zstd.test.ts, and the fetch decompression regressions; cargo check and clippy clean including the x86_64-pc-windows-msvc target.

@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.

I reviewed this PR and didn't find any bugs. Because it consolidates the buffer-bounds validation on the native write/writeSync path across three codecs, reorders zstd's init sequence, and deletes cross-platform FFI surface, a maintainer look would still be worthwhile.

What was reviewed:

  • parse_write_args traced against both original write/writeSync bodies — validation order, error codes/messages, and the reject_resizable split match the pre-refactor code; in_validated.is_none() is equivalent to the old arguments[1].is_null() gate on the pin/unpin paths.
  • validate_mode / validate_uint32_array / validate_write_result_array compared against all three inline originals — equivalent (the Brotli RangeErrorOptions::default() vs. msg: b"" difference is a no-op).
  • map_init_return_code / regrow_output_tail: the implicit drop on the ? error path runs the same inflateEnd/deflateEnd the explicit drop(zlib_reader) did; regrow_output_tail(.., usize::MAX) reduces exactly to the old unbounded compressor path.
  • Nothing outside src/zlib/lib.rs referenced bun_zlib_sys::{posix,win32}; the deleted externs are all re-declared locally in src/zlib/lib.rs.
Extended reasoning...

Overview

Net -93 line refactor across the node zlib native bindings and the bun_zlib / bun_zlib_sys crates. Four shared validation helpers (validate_mode, validate_uint32_array, validate_write_result_array, unset_task_callback) and one shared 7-argument parser (parse_write_args) replace three-way duplicated code in NativeZlib/NativeBrotli/NativeZstd. In src/zlib/lib.rs, map_init_return_code and regrow_output_tail collapse repeated init/regrow blocks, and uInt::try_from(input.len()) guards replace truncating as casts. src/zlib_sys/{posix,win32}.rs are deleted (dead re-exports). One deliberate behavior change: NativeZstd::init now validates initParamsArray before s.init() allocates the ZSTD context. ~220 lines of new tests pin the exact error contract.

Security risks

The consolidated code sits directly on the path that validates user-controlled in_off/in_len/out_off/out_len against JS ArrayBuffer lengths before slicing into their backing stores — a classic OOB-write surface. I traced parse_write_args against both originals and the order/semantics are preserved (bounds compared in usize before any slice is formed; the reject_resizable flag correctly reproduces the async-only rejection from #36555). The uInt::try_from guards close a >4 GiB truncation on avail_in and a Windows-only deflateBound panic. No new attack surface is introduced.

Level of scrutiny

High. This is native FFI code that hands raw pointers into JS-owned memory to C compression libraries, across three codecs and both platforms. REVIEW.md's memory-safety and refactor-is-guilty-until-proven-behavior-preserving sections apply directly. The refactor is careful and the new tests pin every error message, but a subtle divergence here would be an OOB write, not a wrong error string — the class of bug the repo's guidelines flag as most-blocked.

Other factors

All prior review threads (CodeRabbit's jsv_to_u32 note, my two inline nits on the stderr assertion and the stale description paragraph) are resolved, and the description now accurately reflects the zstd reorder. The PR was just brought up to date with main at a maintainer's request and CI is running on the merged head. The deduplication itself is mechanically verifiable and I found no divergence, but the combination of a memory-safety-critical path, one intentional behavior change, and cross-platform FFI deletion puts this outside what I'd auto-approve.

@alii

alii commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@robobun fix the Source lints failure.

The sweep lint read src/zlib_sys/win32.rs to check three dead externs had
not come back; this branch removes the file, so the entries have nothing
to read.
@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixed in 69ddbb7. The failing lint was test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts: one of main's dead-code sweeps added three entries that read src/zlib_sys/win32.rs to make sure gzprintf/inflateUndermine/deflateTune had not come back, and this branch deletes that file, so the read threw ENOENT. Removed those three entries; deleting the file covers what they were guarding. The full source-lints suite passes locally with the same command the workflow runs (75/75), and the check is re-running along with the rest of CI on build 91697.

alii and others added 2 commits August 10, 2026 18:00
They came back in 69ddbb7 by way of a stale index; lib.rs only declares
shared, so nothing referenced them.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

One more commit on top of your merge: c072c5e. While checking that the merge left the PR intact I found that my lint fix (69ddbb7) had accidentally re-added src/zlib_sys/posix.rs and win32.rs; a stale index from an earlier verification step got swept into that commit. They were orphaned (lib.rs only declares shared, so nothing compiled them, which is why every check stayed green), but they are the files this PR exists to delete. Removed again, and the diff against main is back to the intended shape: helper dedup, shared write parser, zlib_sys split removed, zstd init ordering, length guards, lint entry, tests. Re-verified on the pushed tree: native and Windows cargo check, source lints 75/75, handle tests 48/48 (zstd test still fails with main's src), zlib.test.js and zstd.test.ts green.

Comment thread src/zlib_sys/lib.rs

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/zlib/lib.rs`:
- Around line 180-185: Clamp every Vec-derived avail_out value to uInt::MAX as
usize before casting to uInt, including the assignments in regrow_output_tail
and the corresponding sites around lines 271, 756, and 794. Preserve any
existing budget or capacity limits, applying the uInt maximum as an additional
upper bound to prevent truncation.

In `@test/js/node/zlib/zlib-handle-bounds-check.test.ts`:
- Around line 119-140: Extend the mode validation matrix in the
test.each(modeCases) constructor test to cover undefined, zero, NaN, Infinity,
and -Infinity for every native constructor, asserting the shared validator’s
expected errors. Keep the existing string, fractional, and range checks, and
ensure zero is tested explicitly rather than only through min - 1.
- Around line 290-293: Update the invalid-initialization cases in the zlib
bounds-check test to assert that each NativeZlib, NativeBrotli, and NativeZstd
init() call throws instead of silently catching exceptions. Preserve the
existing invalid arguments and callback setup, and ensure an unexpected
successful init fails the test before the lifecycle continuation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75bea08a-b8e6-42a6-a3a4-94356da6b4e4

📥 Commits

Reviewing files that changed from the base of the PR and between c63dc64 and c072c5e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • src/runtime/node/node_zlib_binding.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • src/runtime/node/zlib/NativeZlib.rs
  • src/runtime/node/zlib/NativeZstd.rs
  • src/zlib/Cargo.toml
  • src/zlib/lib.rs
  • src/zlib_sys/lib.rs
  • src/zlib_sys/posix.rs
  • src/zlib_sys/win32.rs
  • test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts
  • test/js/node/zlib/zlib-handle-bounds-check.test.ts
💤 Files with no reviewable changes (5)
  • src/zlib/Cargo.toml
  • src/zlib_sys/posix.rs
  • test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts
  • src/zlib_sys/lib.rs
  • src/zlib_sys/win32.rs

Comment thread src/zlib/lib.rs
Comment thread test/js/node/zlib/zlib-handle-bounds-check.test.ts
Comment thread test/js/node/zlib/zlib-handle-bounds-check.test.ts Outdated
reserve_expand_tail returns the Vec's whole slack, so a buffer with 4 GiB
or more spare truncated avail_out and could make a valid compress fail
with ShortRead. Clamp at the two live sites (the regrow helper and the
compressor's post-deflateBound setup).

Tests: cover undefined, 0, NaN and the infinities for every constructor,
and make the lifecycle test assert each invalid init() really threw.
Comment thread test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts
The orphaned-files lint reads the committed tree, so it catches the
stale-index resurrection that happened once already on this branch.

@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.

I reviewed the latest revision (87a9140) and didn't find any bugs. The prior concerns from this thread — the accidentally-resurrected zlib_sys/{posix,win32}.rs, the missing gone[] guard, the unclamped avail_out casts, the mode/NaN test coverage, and asserting init() actually throws in the lifecycle test — are all addressed by c072c5e, 4562fd9, and 87a9140. Because this reworks the shared write/writeSync argument parsing that hands ArrayBuffer slices across the threadpool boundary and reorders NativeZstd::init, a human look before merge is still worthwhile.

What was reviewed:

  • parse_write_args preserves the original per-path validation order (arg parsing → throw_unless_idle → pin) and error class/code/message for both write and writeSync; the reject_resizable flag correctly scopes the async-only resizable-ArrayBuffer rejection.
  • validate_mode / validate_uint32_array / validate_write_result_array match the three inlined originals byte-for-byte on error text; RangeErrorOptions now uses msg: b"" uniformly (NativeBrotli previously used ..Default::default(), same effect).
  • map_init_return_code / regrow_output_tail / avail_out_for are equivalent to the blocks they replace, plus the new uInt::MAX clamps and avail_in overflow rejection; the dropped explicit drop(zlib_reader) calls are covered by ? unwinding.
  • zlib_sys/posix.rs and win32.rs are deleted at HEAD and now listed in the gone[] orphaned-files guard.
Extended reasoning...

Overview

This PR deduplicates argument-validation helpers shared by NativeZlib/NativeBrotli/NativeZstd (constructor mode, writeResult/writeState, Uint32Array params, and the 7-argument write/writeSync parser) into node_zlib_binding.rs; extracts map_init_return_code/regrow_output_tail/avail_out_for in src/zlib/lib.rs; deletes the unused bun_zlib_sys::{posix,win32} modules and the bun_io dep from bun_zlib; and adds two hardenings (zstd initParamsArray validated before context allocation, and 32-bit avail_in/avail_out overflow guards). 12 files, ~575+/655−, with 234 lines of new tests pinning error class/code/message.

Security risks

The touched code parses user-controlled offsets/lengths into JS ArrayBuffer backing stores and hands raw slices to a threadpool worker. The refactor does not weaken any existing bounds check — the in_off + in_len / out_off + out_len checks and the resizable-ArrayBuffer rejection are preserved verbatim in parse_write_args, and the new tests assert them. The avail_in/avail_out 32-bit clamps close a truncation edge case rather than opening one. No new unsafe blocks are introduced in the binding layer; the SAFETY comments in src/zlib/lib.rs are carried over unchanged. I see no injection, auth, or data-exposure surface here.

Level of scrutiny

High. This is native Rust that manipulates raw pointers into GC-managed ArrayBuffers across a threadpool boundary, and REVIEW.md flags memory safety as the most-blocked category. The refactor is described as behavior-preserving, but it also carries two intentional behavior changes (zstd init ordering, >4 GiB input rejection), and the shared parser now serves both the sync and async paths with a boolean flag distinguishing them. That is exactly the shape where a subtle divergence between the two original copies could be silently unified the wrong way — the PR handles this correctly (reject_resizable), but it warrants a maintainer's eyes.

Other factors

The PR has been through 22 iterations over two months with four merge-from-main resolutions absorbing #32762, #33909, #34427, and #36555. A maintainer is actively driving it toward merge. Every prior inline comment on this thread (mine and CodeRabbit's) is resolved by the last three commits, and the multi-agent bug hunter found nothing on this revision. The new test file pins error messages for all three codecs and includes a regression test that fails on main. Given the scope and the memory-safety-adjacent code paths, deferring rather than auto-approving.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants