Skip to content

Bun.gzipSync/deflateSync: throw invalid-argument for out-of-range libdeflate level - #34114

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/e20c378a/libdeflate-level-oom
Jul 15, 2026
Merged

Bun.gzipSync/deflateSync: throw invalid-argument for out-of-range libdeflate level#34114
Jarred-Sumner merged 1 commit into
mainfrom
farm/e20c378a/libdeflate-level-oom

Conversation

@robobun

@robobun robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Bun.gzipSync / Bun.deflateSync with { library: "libdeflate" } threw "Out of memory" when given a compression level outside libdeflate's [0, 12] range, including level: -1 (zlib's Z_DEFAULT_COMPRESSION, accepted by the same call without the library option).

Bun.gzipSync(buf, { library: "libdeflate", level: -1 });
// before: RangeError: Out of memory
// after:  TypeError: Compression level must be between 0 and 12 for libdeflate

Cause

libdeflate_alloc_compressor returns NULL both for real allocation failure and for an out-of-range level (documented in vendor/libdeflate/libdeflate.h). gzip_or_deflate_sync mapped every None from OwnedCompressor::new to throw_out_of_memory(), so an argument error surfaced as a fake OOM.

Fix

Range-check the level against new MIN_COMPRESSION_LEVEL / MAX_COMPRESSION_LEVEL constants in bun_libdeflate_sys before calling the allocator, and throw an invalid-argument error that names the valid range. The remaining None from the allocator is now only reachable on real OOM.

How did you verify your code works?

New tests in test/js/node/zlib/zlib.test.js cover both gzipSync and deflateSync:

  • levels -2, -1, 13, 100 throw with a message containing "Compression level must be between 0 and 12" and not "memory"
  • levels 0, 1, 6, 9, 12 compress and round-trip through the matching decompressor

Fails on released bun with "Out of memory", passes on this branch.


[review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 2 failed, 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/zlib/zlib.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3306df161)

test/js/node/zlib/zlib.test.js:
(pass) prototype and name and constructor > Gzip > Gzip.prototype should be instanceof Gzip.__proto__ [1.78ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.constructor should be Gzip [1.52ms]
(pass) prototype and name and constructor > Gzip > Gzip.name should be Gzip [1.12ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.__proto__.constructor.name should be Zlib [1.83ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype should be instanceof Gunzip.__proto__ [0.50ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.constructor should be Gunzip [0.34ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.n
... (truncated)

release without fix: 2 failed, 2 skipped
bun test v1.4.0-canary.1 (1498d7b77)

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 [0.02ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.__proto__.constructor.name should be Zlib [0.02ms]
(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

... (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/mechgate.xml" test/js/node/zlib/zlib.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (3306df161)

test/js/node/zlib/zlib.test.js:
(pass) prototype and name and constructor > Gzip > Gzip.prototype should be instanceof Gzip.__proto__ [2.78ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.constructor should be Gzip [1.54ms]
(pass) prototype and name and constructor > Gzip > Gzip.name should be Gzip [1.10ms]
(pass) prototype and name and constructor > Gzip > Gzip.prototype.__proto__.constructor.name should be Zlib [1.82ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype should be instanceof Gunzip.__proto__ [0.52ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.prototype.constructor should be Gunzip [0.31ms]
(pass) prototype and name and constructor > Gunzip > Gunzip.n
... (truncated)

release with fix: 2 skipped
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 781ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/5] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[1/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[1m�[92m    Blocking�[0m waiting for file lock on build directory
�[1m�[92m   Compiling�[0m bun_libdeflate_sys v0.0.0 (/workspace/bun/src/libdefla
... (truncated)
diff hotspot
src/libdeflate_sys/libdeflate.rs | 12 ++++++++++--
 src/runtime/api/BunObject.rs     | 13 +++++++++++--
 test/js/node/zlib/zlib.test.js   | 30 ++++++++++++++++++++++++++++++
 3 files changed, 51 insertions(+), 4 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                              reads  edits  tests
src/libdeflate_sys/libdeflate.rs      1      2      0
src/runtime/api/BunObject.rs          1      1      0
test/js/node/zlib/zlib.test.js        1      1      0

…deflate level

libdeflate_alloc_compressor returns NULL both on OOM and when the level is
outside [0, 12]. Bun.gzipSync/deflateSync mapped every NULL to
throw_out_of_memory, so passing level:-1 or level:13 with library:'libdeflate'
threw 'Out of memory' instead of an argument error.

Range-check the level before calling the allocator and throw a clear
'Compression level must be between 0 and 12 for libdeflate' error, matching
the zstd path.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 10 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: 53cac3e3-bfe2-4874-98ef-d16fa4babd1e

📥 Commits

Reviewing files that changed from the base of the PR and between 16c5576 and 3306df1.

📒 Files selected for processing (3)
  • src/libdeflate_sys/libdeflate.rs
  • src/runtime/api/BunObject.rs
  • test/js/node/zlib/zlib.test.js

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

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:43 AM PT - Jul 14th, 2026

@robobun, your commit 3306df1 has 1 failures in Build #72699 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34114

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

bun-34114 --bun

@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 — targeted fix that turns a misleading "Out of memory" into a proper invalid-argument error for out-of-range libdeflate levels.

What was reviewed:

  • Range check in gzip_or_deflate_sync: 0..=12 inclusive matches libdeflate's documented libdeflate_alloc_compressor contract; default level.unwrap_or(6) preserved.
  • New MIN/MAX_COMPRESSION_LEVEL constants and doc comments in libdeflate.rs are additive-only; no behavior change to OwnedCompressor::new.
  • Tests cover both gzipSync/deflateSync, boundary levels 0/12 succeed, -1/13 throw with the specific message and not "memory".
  • Checked sibling OwnedCompressor::new call sites (compress_body.rs, audit_command.rs, Archive.rs) — all use fixed or pre-validated levels, so the bug class doesn't reproduce there.
Extended reasoning...

Overview

This PR fixes Bun.gzipSync / Bun.deflateSync with { library: "libdeflate" } to throw a descriptive invalid-argument error when level is outside libdeflate's [0, 12] range, instead of a misleading "Out of memory". Three files touched:

  • src/libdeflate_sys/libdeflate.rs: adds pub const MIN_COMPRESSION_LEVEL = 0 / MAX_COMPRESSION_LEVEL = 12 and updates doc comments to note that libdeflate_alloc_compressor returns NULL for out-of-range levels as well as OOM. No behavioral change.
  • src/runtime/api/BunObject.rs (JSZlib::gzip_or_deflate_sync, Library::Libdeflate arm): hoists level.unwrap_or(6) to a local, range-checks it against the new constants via RangeInclusive::contains, and throws throw_invalid_arguments with a message naming the valid range. The subsequent OwnedCompressor::new(level)throw_out_of_memory() fallback is unchanged and now only reachable on real OOM.
  • test/js/node/zlib/zlib.test.js: new describe block exercising both gzipSync and deflateSync — levels -2, -1, 13, 100 throw with "Compression level must be between 0 and 12" and not "memory"; levels 0, 1, 6, 9, 12 compress and round-trip through the matching decompressor.

Security risks

None. This is input validation on a scalar integer that produces a better error message. No new allocation, no new FFI surface, no user-controlled data reaching a new code path — the range check runs strictly before the existing allocator call.

Level of scrutiny

Low. The change is a ~10-line validate-before-allocate guard plus two named constants and doc-comment updates. The constants (0 and 12) match libdeflate's documented API contract. The error path uses the existing throw_invalid_arguments helper consistent with neighboring code. The default level (6) is unchanged. The inclusive range check is correct at both boundaries, and the tests explicitly exercise 0, 12, -1, and 13.

Other factors

  • Sibling call sites: I grepped for all OwnedCompressor::new callers. audit_command.rs and the cached-handle path in compress_body.rs use hardcoded level 6; Archive.rs maps None to CompressError::GzipInitFailed (not OOM); the custom-level path in compress_body.rs was flagged by a finder and refuted by verifiers as not sharing this bug. None of these are the user-facing Bun.gzipSync/deflateSync path this PR targets.
  • Test quality: Boundary coverage is good (0 and 12 succeed, -1 and 13 fail), both entry points (gzipSync/deflateSync) are covered, and the assertion checks the specific message substring rather than a bare toThrow(). The round-trip test confirms in-range levels still work end-to-end.
  • No CODEOWNERS entries match the touched paths.
  • No outstanding review comments — timeline contains only bot noise (CodeRabbit rate-limit notice, robobun build link).

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

CI: test/js/node/zlib/zlib.test.js (the file this PR changes) passes on all lanes.

Remaining failures are unrelated to this diff:

  • test/js/third_party/grpc-js/test-tonic.test.ts on darwin-14-aarch64: rustup could not choose a version of cargo to run (no default toolchain configured on that box; infra, not code)
  • test/regression/issue/30205.test.ts (LeakSanitizer in napi_body.rs:4513, from napi: keep threadsafe functions alive after their env is torn down #34067; fix in progress elsewhere)
  • test/napi/napi.test.ts (napi_wrap lifetime, Windows)
  • cargo clippy (napi_body.rs:2811 missing safety comment, from napi: keep threadsafe functions alive after their env is torn down #34067; failing on ~10 open PRs)
  • assorted flaky tests also red on main: spawn, bun-install-proxy, webview-chrome, in-process-cron, bun-install, no-orphans, 24388, websocket, 20144, test-net-write-slow

Ready for review.

@Jarred-Sumner
Jarred-Sumner merged commit 7ccf0e8 into main Jul 15, 2026
78 of 81 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/e20c378a/libdeflate-level-oom branch July 15, 2026 00:08
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