Skip to content

node:zlib: accept zstd pledgedSrcSize values of 4 GiB and larger - #33442

Open
robobun wants to merge 4 commits into
mainfrom
farm/145e3845/zstd-pledged-src-size
Open

node:zlib: accept zstd pledgedSrcSize values of 4 GiB and larger#33442
robobun wants to merge 4 commits into
mainfrom
farm/145e3845/zstd-pledged-src-size

Conversation

@robobun

@robobun robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What's broken

pledgedSrcSize is the option you reach for when streaming a file too large to buffer, but pledging 4 GiB or more throws:

const zlib = require("node:zlib");
zlib.createZstdCompress({ pledgedSrcSize: 2 ** 32 });
// RangeError: The value of "pledgedSrcSize" is out of range.
//             It must be >= 0 and <= 4294967295. Received 4294967296
// code: "ERR_OUT_OF_RANGE"

Same error from zstdCompressSync, zstdCompress, and node:zlib/iter's compressZstd. Node accepts it: it only rejects negative values, and writes the pledge straight into the frame header.

Cause

NativeZstd.init validated the option with validate_uint32, capping it at 2**32 - 1. Everything downstream of that validator is already 64-bit: Context.pledged_src_size is a u64, and ZSTD_CCtx_setPledgedSrcSize takes a c_ulonglong. The zstd frame header's Frame_Content_Size field is up to 8 bytes, so sizes above 4 GiB are representable, they just never made it past the validator.

Fix

Validate it as a non-negative safe integer (0 ..= Number.MAX_SAFE_INTEGER, which is the largest integer a JS number holds exactly) and pass the value through unmodified.

Verification

test/js/node/zlib/zstd-pledged-src-size.test.ts pledges 4 GiB through all three compression APIs and reads the frame content size back out of the header, so it checks the value arrives at zstd unmodified rather than just that nothing threw. Finishing with ZSTD_e_flush instead of ZSTD_e_end skips zstd's pledged-vs-actual check, which is what lets the test pledge 4 GiB without feeding it 4 GiB.

Values below the old cap produce byte-identical output to before (and to node), Number.MAX_SAFE_INTEGER is accepted, and one past it, negatives, non-integers and NaN still throw ERR_OUT_OF_RANGE.

before / after
$ bun-1.4.0 test/js/node/zlib/zstd-pledged-src-size.test.ts
(fail) accepts 4 GiB and larger
(fail) accepts 4 GiB and larger when compressing asynchronously
(fail) accepts 4 GiB and larger when streaming
 6 pass, 3 fail

$ bun-debug test/js/node/zlib/zstd-pledged-src-size.test.ts
 9 pass, 0 fail

test/js/node/test/parallel/test-zlib-zstd-pledged-src-size.js, test/js/node/test/parallel/test-stream-iter-validation.js, test/js/node/zlib/zlib.test.js and test/regression/issue/23314/ all still pass.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2b41a28c-7107-40f1-b204-244e4f3054dc

📥 Commits

Reviewing files that changed from the base of the PR and between 9baf49f and 0fb6e2c.

📒 Files selected for processing (2)
  • src/runtime/node/zlib/NativeZstd.rs
  • test/js/node/zlib/zstd-pledged-src-size.test.ts

Walkthrough

This change modifies pledgedSrcSize validation in NativeZstd::init to use integer range checking against MAX_SAFE_INTEGER instead of the prior validate_uint32 conversion path, converting to u64 via try_from. A new test file verifies frame header encoding and error handling for invalid inputs.

Changes

Zstd pledgedSrcSize Validation

Layer / File(s) Summary
Validation logic update
src/runtime/node/zlib/NativeZstd.rs
Numeric pledgedSrcSize input is now validated as an integer within [0, MAX_SAFE_INTEGER] and converted to u64 via try_from, replacing the prior validate_uint32-based path.
Frame header test helpers
test/js/node/zlib/zstd-pledged-src-size.test.ts
Adds frameContentSize to decode ZSTD frame header descriptors, a shared test buffer, and a flushOnly helper to force pledged size into the header.
Sync/async/streaming encoding tests
test/js/node/zlib/zstd-pledged-src-size.test.ts
Verifies pledgedSrcSize is correctly written into frame headers across synchronous, asynchronous, and streaming compression, including 4GiB boundary and large safe-integer values.
Out-of-range input validation tests
test/js/node/zlib/zstd-pledged-src-size.test.ts
Asserts createZstdCompress throws ERR_OUT_OF_RANGE for negative, fractional, infinite, or above-MAX_SAFE_INTEGER pledgedSrcSize values.

Possibly related PRs

  • oven-sh/bun#32762: Also modifies NativeZstd::init in the same file, adding closed-handle checks near the code touched by this pledgedSrcSize validation change.
🚥 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 and accurately summarizes the main change: allowing larger zstd pledgedSrcSize values.
Description check ✅ Passed The description covers the problem, cause, fix, and verification, matching the template's required intent.

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

@github-actions github-actions Bot added the claude label Jul 6, 2026
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:30 AM PT - Jul 6th, 2026

@robobun, your commit 0fb6e2c has 1 failures in Build #68966 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33442

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

bun-33442 --bun

@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

🤖 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/zstd-pledged-src-size.test.ts`:
- Around line 47-58: The streaming Zstd test only rejects on error and only
destroys the encoder on success, so a premature close can hang the test and a
failed path can leak the native handle. Update the test around
createZstdCompress, Promise.withResolvers, and encoder.flush so every failure
signal (at least error and close, plus any other relevant abort/exit path)
rejects the promise, and ensure encoder cleanup is registered before awaiting so
it runs on both success and failure, not just after the assertions.
- Around line 60-69: The rejection test in zstd-pledged-src-size should be split
into `test.each` cases instead of a manual loop so each invalid input reports
independently and matches repo style. Add `NaN` to the invalid `pledgedSrcSize`
matrix and keep the same `createZstdCompress` assertion shape, using the
`zlib.createZstdCompress` path to verify all non-negative safe integer
violations are covered explicitly.
🪄 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: 15ff19ee-e13c-4133-a4a4-4911e4db48a3

📥 Commits

Reviewing files that changed from the base of the PR and between 9f29e55 and 9baf49f.

📒 Files selected for processing (2)
  • src/runtime/node/zlib/NativeZstd.rs
  • test/js/node/zlib/zstd-pledged-src-size.test.ts

Comment thread test/js/node/zlib/zstd-pledged-src-size.test.ts
Comment thread test/js/node/zlib/zstd-pledged-src-size.test.ts Outdated
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both review comments in 938b18b:

  • streaming test: wired close to the rejection path and moved encoder.destroy() into a finally, so a premature close fails with a diagnostic instead of hanging and the handle is released on either path.
  • rejection cases: split into it.each and added NaN. Worth keeping, since the Zstd constructor has no JS-side guard for it, so the native validator is the only thing rejecting it.

Still 3 fail on 1.4.0 / 9 pass with the fix.

@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 — straightforward validator widening from uint32 to safe-integer range for a value the downstream code already handles as u64.

Extended reasoning...

Overview

This PR changes a single validation call in src/runtime/node/zlib/NativeZstd.rs: pledgedSrcSize was previously validated with validate_uint32 (capped at 2³²−1) and is now validated with validate_integer(min=0, max=MAX_SAFE_INTEGER). Everything downstream — Context.pledged_src_size: u64 and ZSTD_CCtx_setPledgedSrcSize(u64) — was already 64-bit, so this is purely relaxing an artificially tight input gate to match Node.js and the zstd frame format (which supports an 8-byte content size field). The u64::try_from(i64) is provably infallible given the validated [0, 2⁵³−1] range.

A new test file covers the sync/async/streaming entry points, verifies the pledged size actually lands in the zstd frame header (not just that no exception is thrown), checks both sides of the old and new boundaries, and asserts ERR_OUT_OF_RANGE for negative/fractional/Infinity/NaN/beyond-safe-integer inputs via it.each.

Security risks

None. pledgedSrcSize is advisory metadata written into the zstd frame header; it does not size any allocation in Bun or in the vendored zstd on the compress path. Widening the accepted range to what the underlying C API natively takes (unsigned long long) introduces no new attack surface.

Level of scrutiny

Low. This is a ~10-line Node-compat fix swapping one existing in-tree validator for another with a wider range. It uses established validators::validate_integer machinery, doesn't touch memory management, threading, or FFI lifetime, and the change is mechanically verifiable against the validator's implementation.

Other factors

Both CodeRabbit inline comments (encoder cleanup in try/finally + wire close to reject; use test.each and add NaN) were addressed in commit 938b18b and are marked resolved. The PR description shows the test failing on the released build and passing on the debug build, and lists the related upstream Node parallel tests as still passing. No bugs were flagged by the bug-hunting system.

robobun and others added 3 commits July 6, 2026 11:37
The option was validated as a uint32, so pledging a source size of 2**32 or
more threw ERR_OUT_OF_RANGE even though ZSTD_CCtx_setPledgedSrcSize takes a
u64 and the frame header's content-size field holds 8 bytes. Validate it as a
non-negative safe integer instead and pass it through unmodified.
Wire close to the rejection path and destroy the encoder in a finally, so a
premature close fails with a diagnostic instead of hanging and the handle is
released on both paths. Split the rejected values into it.each cases and add
NaN, which only the native validator rejects.
@robobun
robobun force-pushed the farm/145e3845/zstd-pledged-src-size branch from 938b18b to ac2502b Compare July 6, 2026 11:41

@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 — straightforward validator swap that lifts the artificial 4 GiB cap on pledgedSrcSize, with thorough header-round-trip tests.

Extended reasoning...

Overview

This PR touches two files: a ~10-line change in src/runtime/node/zlib/NativeZstd.rs that swaps validate_uint32 for validate_integer(min=0, max=MAX_SAFE_INTEGER) when parsing the pledgedSrcSize option in NativeZstd::init, and a new 72-line test file test/js/node/zlib/zstd-pledged-src-size.test.ts. Everything downstream of the validator was already 64-bit (Context.pledged_src_size: u64, ZSTD_CCtx_setPledgedSrcSize takes a c_ulonglong, and the zstd frame header's Frame_Content_Size field is up to 8 bytes), so the u32 cap was purely an artifact of the wrong validator being used.

Security risks

None. pledgedSrcSize is metadata written into the zstd frame header — it does not drive any allocation in Bun. Widening the accepted range from [0, 2^32-1] to [0, 2^53-1] cannot cause overflow (validate_integer returns an i64 in [0, MAX_SAFE_INTEGER], and u64::try_from on a non-negative i64 is infallible, as the .expect() correctly notes). No auth, crypto, permissions, or untrusted-input parsing beyond the existing numeric validation, which is strictly tighter than Node's (Node only rejects negatives).

Level of scrutiny

Low. This is a focused Node-compat fix in an option-parsing path with no control-flow changes: same is_number() gate preserving the undefined → ZSTD_CONTENTSIZE_UNKNOWN default, same error-handling structure, just a wider validator. The validate_integer helper is the established in-tree validator used throughout src/runtime/node/ for exactly this case. No memory management, GC, threading, or FFI lifetime changes.

Other factors

The test coverage is unusually thorough for a change this size: it decodes the zstd frame header per the spec and asserts the pledged size actually lands in Frame_Content_Size unmodified (rather than just "didn't throw"), across sync/async/streaming APIs, at the old boundary (2^32-1), just past it (2^32), and at MAX_SAFE_INTEGER. Rejection cases use it.each over MAX_SAFE_INTEGER+1, -1, 1.5, Infinity, and NaN. Both CodeRabbit review comments (try/finally cleanup + close wiring in the streaming test; test.each + NaN case) were addressed and the threads are resolved. The bug-hunting system found no issues. The PR description confirms 3 fail on 1.4.0 / 9 pass with the fix, and that the existing zstd/zlib test suites still pass.

@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 — clean validator swap with thorough frame-header verification tests.

Extended reasoning...

Overview

This PR widens the accepted range for zstd's pledgedSrcSize option from [0, 2^32-1] to [0, Number.MAX_SAFE_INTEGER], matching Node.js behavior. The native change in src/runtime/node/zlib/NativeZstd.rs is 8 lines: swap validators::validate_uint32 for validators::validate_integer with explicit Some(0)/Some(jsc::MAX_SAFE_INTEGER) bounds, then u64::try_from the resulting i64. Everything downstream (Context.pledged_src_size: u64, ZSTD_CCtx_setPledgedSrcSize taking c_ulonglong) was already 64-bit, so the old validator was the only bottleneck. A new 72-line test file covers sync/async/streaming entry points and rejection cases.

Security risks

None. pledgedSrcSize is a hint written into the zstd frame header — it doesn't size any allocation or buffer in Bun's code. Widening the accepted range from 32-bit to 53-bit for a value that's already stored and passed as 64-bit introduces no overflow or resource-exhaustion vector. Invalid inputs (negative, non-integer, NaN, >MAX_SAFE_INTEGER) still throw ERR_OUT_OF_RANGE via the existing validator machinery.

Level of scrutiny

Low-to-moderate. This is a targeted Node.js-compat fix in the input-validation layer with no changes to the compression logic itself. I verified validate_integer returns JsResult<i64> and enforces the passed bounds, so u64::try_from on a value in [0, 2^53-1] is genuinely infallible as the .expect claims. The is_number() gate before the validator preserves the existing "undefined → ZSTD_CONTENTSIZE_UNKNOWN" default.

Other factors

The test quality is high: rather than just asserting nothing throws, it decodes the zstd frame header per the spec and checks the pledged size actually landed in Frame_Content_Size, across all three API surfaces. The ZSTD_e_flush trick to skip zstd's pledged-vs-actual check (so 4 GiB can be pledged without feeding 4 GiB) is well-documented in the test. Both CodeRabbit suggestions (wire close to reject + try/finally cleanup; it.each + NaN case) were addressed and are visible in the final diff. The PR description confirms before/after test results and that adjacent zlib/zstd tests still pass. No bugs were found by the bug-hunting system.

@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

The red lanes on this PR are not from this diff. Summary for whoever picks it up, updated now that the build has finished.

Final result of the latest build: 144 test-bun lanes pass. Every failure is a darwin aarch64 lane, and none of them is a test failing:

  • :darwin: 26 aarch64 - test-bun (x2), which dies before running a single test:

    Error: buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun'.
    Refusing to continue with a partial download (would silently fall back to the wrong binary).
    

    The darwin aarch64 - build-bun step itself passes, so the artifact exists. The test agent just cannot pull it inside the 120s window, and the runner correctly refuses to proceed.

  • :darwin: 14 aarch64 - test-bun, on test/js/web/websocket/autobahn.test.ts, where the Autobahn service cannot start because its image is amd64 only:

    autobahn  The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)
    autobahn-1  | exec /opt/pypy/bin/wstest: exec format error
    container bun-test-services-autobahn-1 exited (255)
    

Previously red, now green: two earlier failures, both confirmed unrelated and both gone:

  • test/js/bun/cookie/cookie-map.test.ts on every lane. The branch predated 48ff9eb ("cookie: update remaining cookie-map Expires assertions to IMF-fixdate", cookie: update remaining cookie-map Expires assertions to IMF-fixdate #33425), which fixes the three stale Expires assertions that 028f210 had broken. Rebasing onto main cleared it.
  • test/js/node/test/parallel/test-worker-message-port-transfer-terminate.js, SIGABRT on a JSC !scope.exception() || !hasSlot assertion, x64-asan only. A worker-terminate race, in a file already listed in test/no-validate-leaksan.txt. It passed on the next run. A pledgedSrcSize option validator is not reachable from that test, which runs in its own process and never constructs a zstd stream.

The diff is green. Every build lane passes, and test/js/node/zlib/zstd-pledged-src-size.test.ts passes on release builds across debian 13 aarch64, ubuntu 25.04 aarch64, and debian 13 x64, alongside the existing zlib and zstd suites. Locally it is 3 fail on 1.4.0 and 9 pass with the fix.

Not pushing again, since only a push re-runs CI and there is nothing left to change. Happy to rebase or re-run if that helps.

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.

1 participant