Skip to content

Hardening: input validation and bounds tightening across 31 subsystems (round 3)#31221

Merged
Jarred-Sumner merged 104 commits into
mainfrom
claude/hardening-sweep-3
May 24, 2026
Merged

Hardening: input validation and bounds tightening across 31 subsystems (round 3)#31221
Jarred-Sumner merged 104 commits into
mainfrom
claude/hardening-sweep-3

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Round 3 of the input-validation and bounds-tightening sweep. 56 commits across 31 subsystems, each commit scoped to one change:

  • install (6): dependency folder name validation, migration skip predicates, manifest name copying, binary lockfile patch entries, off-registry integrity requirements, symlink target normalization
  • fetch / http client (5): proxy response handling, content-length parsing, connection pool keying, cross-origin redirect header handling, host override reset
  • node:http / uws (4): parser execute handling, server socket receiver checks, buffered write ordering, parser/data-handler interaction
  • mysql (4): handshake packet handling, packet payload bounds, null marker decoding, temporal value ranges
  • http2 (3): header name buffer sizing, response header value validation, frame padding bounds
  • shell (3): interpolated value tracking in the parser, seq loop termination, rm directory opening
  • node:zlib (2): write state validation, write buffer lifetime across async work
  • sql / postgres / sqlite (4): transaction option validation, array type name validation, array literal parsing, statement revalidation around bind callbacks
  • structuredClone (2): constant pool index bounds, view backing store tag validation
  • node:fs (2): async read buffer pinning, vector buffer snapshotting
  • strings (2): hashed string equality, utf16 length conversion
  • node:crypto (2): cipher option handling, signature encoding conversion
  • one each: blob, dns, formdata, fs.watch, glob, node:vm, resolver, router, serve (2), sourcemap, spawn, streams, timers, tls, url, websocket

Verified with cargo check --workspace on all 10 CI target triples and the per-subsystem test suites for each touched area; no new test failures relative to a baseline debug build.

@robobun

robobun commented May 22, 2026

Copy link
Copy Markdown
Collaborator
Updated 1:10 PM PT - May 23rd, 2026

@Jarred-Sumner, your commit f9bb063 has some failures in Build #57335 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 31221

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

bun-31221 --bun

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR applies broad safety and validation hardening: socket pooling and redirect TLS handling, HTTP/H2 header and framing checks, parser early-stop on handler closure, buffer pinning for async/native paths, lockfile and install path validation, SQL/type bounds checks, JSC/FFI receiver and deserialization guards, and numerous parser/bounds fixes.

Changes

Hardened Security & Safety Across Sockets, Buffers, Lockfiles, Protocols, and Input Validation

Layer / File(s) Summary
TLS Socket Pool & Redirect Handling
src/http/HTTPContext.rs, src/http/lib.rs
Pool key for direct TLS now includes Host/SNI override hash; sockets closed (not pooled) when hostname override changed across redirects; proxy CONNECT failures prevent follow-up redirects; per-request Host override cleared on cross-origin redirects.
HTTP Parser & WebSocket Inflate Safety
packages/bun-uws/src/HttpParser.h, packages/bun-uws/src/PerMessageDeflate.h
Parser dispatch paths now observe dataHandler return value and stop dispatching when handler indicates shutdown; permessage-deflate fast path enforces maxPayloadLength.
JSC Host Bindings & Deserialization Guards
src/jsc/bindings/*, src/jsc/bindings/webcore/SerializedScriptValue.cpp
Replaced unchecked casts with dynamicDowncast and added throw-scope/type checks for host getters/setters; signature conversion failure short-circuits verification; deserialization validates tags and pool indices.
ArrayBuffer Pinning and Async Buffer Lifetime
src/runtime/node/node_fs.rs, src/runtime/node/node_zlib_binding.rs, src/runtime/node/zlib/*, src/runtime/image/Image.rs, src/runtime/webcore/Blob.rs, src/runtime/webcore/ByteStream.rs, src/runtime/node/types.rs
Introduces pin/unpin ABI, PinnedWriteBuffers holder, pins ArrayBuffer backing stores during async/native worker operations, copies or re-derives slices from JS roots to avoid writes into detached storage.
Lockfile & Installer Path Safety
src/install/dependency.rs, src/install/lockfile/*, src/install/TarballStream.rs, src/install/PackageManager/PackageManagerEnqueue.rs, src/install/migration.rs
Adds is_safe_install_folder_name and uses it in lockfile/tree processing; symlink target normalization uses relative normalization and rejects .. escapes; registry containment checks enforce integrity requirements; patched-dep flag validated as u8.
SQL & Protocol Validation
src/js/internal/sql/*, src/sql/mysql/protocol/*, src/sql_jsc/*
Transaction option strings validated; PostgreSQL array type names constrained; DateTime/Time range checks added; MySQL packet payload and NULL-marker handling tightened; handshake-tail checks for TLS upgrade.
Input Validation & Bounds Fixes
src/base64/lib.rs, src/bun_core/string/HashedString.rs, src/glob/matcher.rs, src/http/h2_client/dispatch.rs, src/resolver/package_json.rs, src/runtime/api/bun/h2_frame_parser.rs, src/shell_parser/parse.rs, src/url/lib.rs, src/runtime/webcore/FormData.rs
Bounds for VLQ decoding; byte-for-byte equality added to hashed string equality; glob multibyte bounds; response header value validation; ES module wildcard subpath sanitization; H2 padding boundary change; shell parser tracks interpolated JS ranges; query-string param cap (2048); FormData content-type printable-ASCII guard.
Server Request & Misc Socket Safety
src/runtime/server/RequestContext.rs, src/runtime/socket/uws_jsc.rs, src/runtime/node/win_watcher.rs, src/runtime/shell/builtin/*, src/spawn/static_pipe_writer.rs, src/runtime/timer/timer_object_internals.rs, src/runtime/dns_jsc/dns.rs, src/runtime/api/filesystem_router.rs
Tracks streamed request-body length and enforces max_request_body_size with 413; defers write buffer materialization until after JS coercion; per-VM path watcher manager allocation; NOFOLLOW on directory open; seq float advancement guard; timer maps cleanup; DNS cache key matching includes hostname bytes; filesystem router preserves decoded storage lifetime.
  • Possibly related PRs:

  • Suggested reviewers:

    • alii
    • RiskyMH

@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: 11

🤖 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/http_types/URLPath.rs`:
- Around line 61-68: The method take_decoded_storage() on URLPath relinquishes
the Box<[u8]> but leaves slice fields (path, pathname, extname, etc.) pointing
into that buffer, creating a dangling-slice hazard; mark the function
#[must_use] to warn callers if they ignore the returned storage and, immediately
after calling self._decoded_storage.take(), clear/invalidate the slice fields on
self (set them to empty slices/None as appropriate) so the URLPath cannot be
used while ownership of the backing storage has moved; refer to the
take_decoded_storage method, the _decoded_storage field, and the slice fields
(path, pathname, extname) when implementing this change.

In `@src/http/lib.rs`:
- Around line 4443-4445: The code currently converts proxy CONNECT failures into
UnexpectedRedirect in the FetchRedirect::Error handling, causing callers to lose
the proxy response/body; update the FetchRedirect::Error branch (the logic that
produces UnexpectedRedirect) to first check is_proxy_connect_failure and, if
true, preserve and return the proxy's response/status/body (or an error variant
that carries the proxy response) instead of mapping it to
UnexpectedRedirect—ensure references to is_proxy_connect_failure,
self.redirect_type/FetchRedirect::Follow, and the
FetchRedirect::Error/UnexpectedRedirect conversion are guarded so proxy CONNECT
failures are surfaced as-is.
- Around line 4254-4267: The header parsing arm that checks
hash_header_const(b"Content-Length") currently treats any invalid Content-Length
as an unrecoverable framing error (returning err!(InvalidHTTPResponse)), which
breaks CONNECT tunnel handling; change the logic so that when parsing response
headers for a successful 200 CONNECT (i.e. the code path that leads to proxy
tunneling / tunnel mode) you ignore framing headers like Content-Length and
Transfer-Encoding instead of validating them. Locate the header match arm that
uses hash_header_const(b"Content-Length") and
bun_core::parse_unsigned::<usize>() and add a conditional to skip the
parse-and-error behavior when the response is for a CONNECT tunnel (or when the
proxy_tunneling/tunnel-mode flag is set), leaving existing validation intact for
normal responses.

In `@src/install/lockfile/bun.lock.rs`:
- Around line 70-78: The serialization path in save_from_binary_inner is using a
raw prefix check to collapse npm registry URLs to "" which mismatches the
stricter registry-boundary logic in url_is_under_registry used by
parse_into_binary_lockfile, causing off-registry hosts like
"registry.npmjs.org.evil" to be mis-collapsed; update save_from_binary_inner to
call or reuse url_is_under_registry (the same boundary check) when deciding to
collapse a URL to the registry shorthand so only true under-registry URLs are
collapsed and npm_url_needs_integrity semantics are preserved.

In `@src/install/lockfile/Tree.rs`:
- Around line 303-317: The current folder_name_is_safe in Tree.rs is weaker than
crate::dependency::is_safe_install_folder_name (it allows empty names/segments
and misses colon checks) causing inconsistent validation between
relative_path_and_depth and process_subtree; fix by replacing the custom checks
in folder_name_is_safe to delegate to the canonical validator
(crate::dependency::is_safe_install_folder_name) so all callers (including
relative_path_and_depth) use the same stricter logic and avoid empty
segment/empty name/drive-colon edge cases.

In `@src/install/migration.rs`:
- Around line 608-620: Extract the duplicated skip logic (checking
ExprData::EBoolean for keys b"inBundle" or b"extraneous" on a pkg map) into a
small helper like should_skip_pkg(pkg: &ExprMap) -> bool (or is_skipped_pkg) and
replace the inline predicate in the counting, building, and linking passes with
a call to that helper (update the sites that currently inspect
pkg.get(b"inBundle")/... and pkg.get(b"extraneous")/... to call the new function
instead). Ensure the helper returns true when either key is present and true,
preserves the current unwrap_or(false) semantics, and is used consistently by
the three passes to prevent drift.

In `@src/runtime/node/win_watcher.rs`:
- Around line 509-512: Before storing the new PathWatcherManager returned by
PathWatcherManager::init into DEFAULT_MANAGER, detect the existing manager (the
pointer in DEFAULT_MANAGER or the `existing` variable) and retire it: if the old
manager is empty call its immediate teardown method (e.g., deinit/destroy)
otherwise mark it for deferred deinit (set deinit_on_last_watcher or call a
mark_for_deferred_deinit method) so unregister_watcher can finish cleanup later;
then store the new manager. Ensure you reference DEFAULT_MANAGER,
PathWatcherManager::init, unregister_watcher, and deinit_on_last_watcher in your
change so the previous manager is not leaked when replacing the global slot.

In `@src/runtime/node/zlib/NativeBrotli.rs`:
- Around line 198-210: The current code in NativeBrotli (around the handling of
arguments.ptr[1]) uses .unwrap() on arguments.ptr[1].as_array_buffer and then
accepts any view before treating it as a Uint32Array, which can panic; change
this to mirror the NativeZstd guard: validate that arguments.ptr[1] is present,
call as_array_buffer and if that returns None return a thrown Error
(INVALID_ARG_VALUE) instead of unwrapping, then check that the buffer view is a
Uint32Array (use the same type-check used in NativeZstd) and that its length >=
2 before calling as_u32/as_mut_ptr; on any failure return a descriptive Error
via global_this.err().throw() so bad JS input returns an error rather than
aborting (refer to flush_write_result, write_result,
arguments.ptr[1].as_array_buffer, and write_result_slice.as_u32).

In `@src/runtime/node/zlib/NativeZlib.rs`:
- Around line 147-159: The code currently unwraps the caller-provided buffer
when reading arguments.ptr[4] via as_array_buffer and then assumes as_u32
succeeds, which can panic; change this to return a JS error instead of panicking
by checking and handling both failures: validate that
arguments.ptr[4].as_array_buffer(global) returns Some and that the resulting
buffer can be viewed as a Uint32Array (as_u32) before using it; if either step
fails, return a bun_jsc::ErrorCode::INVALID_ARG_VALUE (or an appropriate JS
error) with a clear message about writeResult needing to be a Uint32Array of at
least 2 elements, and only then call as_mut_ptr() and proceed to
flush_write_result.

In `@src/runtime/server/RequestContext.rs`:
- Around line 3576-3582: The limit check for streaming bodies is missing bytes
already drained from request_body_buf when streaming starts: seed
request_body_streamed_len with the length of the drained buffer inside
on_start_streaming_request_body() (use request_body_buf.len() or the exact
drained byte count) so that request_body_streamed_len reflects buffered +
subsequent chunk bytes before comparing against
server.config().max_request_body_size; update the code paths that transition
from buffered→streaming to initialize request_body_streamed_len accordingly.

In `@src/url/lib.rs`:
- Around line 1066-1068: The pre-scan currently counts all parameters and bytes
before the capped second pass, allowing large allocations; update the pre-scan
loops that compute `count` and `estimated_str_len` so they stop incrementing
once `count` reaches `MAX_QUERY_STRING_PARAMS` (and similarly cap
`estimated_str_len` to a safe upper bound) and use those capped values when
calling `list.reserve(count)` / `Vec::with_capacity(estimated_str_len)`;
specifically modify the pre-scan logic that feeds `list.reserve` and
`Vec::with_capacity` (referencing `list.reserve`, `Vec::with_capacity`, and
`MAX_QUERY_STRING_PARAMS`) to bound both the parameter count and the estimated
allocation to the defined hard caps.
🪄 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: 49ce076c-32f3-4595-9e61-57bf39c42339

📥 Commits

Reviewing files that changed from the base of the PR and between 346ce08 and 0ddf9df.

📒 Files selected for processing (57)
  • packages/bun-usockets/src/crypto/openssl.c
  • packages/bun-uws/src/HttpParser.h
  • packages/bun-uws/src/PerMessageDeflate.h
  • src/base64/lib.rs
  • src/bun_core/string/HashedString.rs
  • src/glob/matcher.rs
  • src/http/HTTPContext.rs
  • src/http/h2_client/dispatch.rs
  • src/http/lib.rs
  • src/http_types/URLPath.rs
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • src/install/TarballStream.rs
  • src/install/dependency.rs
  • src/install/lockfile/Tree.rs
  • src/install/lockfile/bun.lock.rs
  • src/install/lockfile/bun.lockb.rs
  • src/install/migration.rs
  • src/js/internal/sql/mysql.ts
  • src/js/internal/sql/postgres.ts
  • src/js/internal/sql/sqlite.ts
  • src/jsc/bindings/BunString.cpp
  • src/jsc/bindings/NodeVMModule.cpp
  • src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
  • src/jsc/bindings/node/crypto/JSCipherConstructor.cpp
  • src/jsc/bindings/node/crypto/JSVerify.cpp
  • src/jsc/bindings/node/http/NodeHTTPParser.cpp
  • src/jsc/bindings/sqlite/JSSQLStatement.cpp
  • src/jsc/bindings/webcore/SerializedScriptValue.cpp
  • src/resolver/package_json.rs
  • src/runtime/api/bun/h2_frame_parser.rs
  • src/runtime/api/filesystem_router.rs
  • src/runtime/dns_jsc/dns.rs
  • src/runtime/image/Image.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/node/node_zlib_binding.rs
  • src/runtime/node/types.rs
  • src/runtime/node/win_watcher.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • src/runtime/node/zlib/NativeZlib.rs
  • src/runtime/node/zlib/NativeZstd.rs
  • src/runtime/server/RequestContext.rs
  • src/runtime/shell/builtin/rm.rs
  • src/runtime/shell/builtin/seq.rs
  • src/runtime/socket/uws_jsc.rs
  • src/runtime/timer/timer_object_internals.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/ByteStream.rs
  • src/runtime/webcore/FormData.rs
  • src/shell_parser/parse.rs
  • src/spawn/static_pipe_writer.rs
  • src/sql/mysql/protocol/NewWriter.rs
  • src/sql/mysql/protocol/PacketHeader.rs
  • src/sql_jsc/mysql/MySQLConnection.rs
  • src/sql_jsc/mysql/MySQLValue.rs
  • src/sql_jsc/mysql/protocol/ResultSet.rs
  • src/sql_jsc/postgres/DataCell.rs
  • src/url/lib.rs

Comment thread src/http_types/URLPath.rs
Comment thread src/http/lib.rs
Comment thread src/http/lib.rs
Comment thread src/install/lockfile/bun.lock.rs
Comment thread src/install/lockfile/Tree.rs Outdated
Comment thread src/runtime/node/win_watcher.rs
Comment thread src/runtime/node/zlib/NativeBrotli.rs
Comment thread src/runtime/node/zlib/NativeZlib.rs
Comment thread src/runtime/server/RequestContext.rs Outdated
Comment thread src/url/lib.rs
Comment thread src/runtime/node/win_watcher.rs
Comment thread src/js/internal/sql/postgres.ts Outdated

@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: 14

🤖 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/cli/install/bun-lock.test.ts`:
- Around line 698-705: Reorder the assertions so the subprocess exit code is
checked before reading filesystem artifacts: after collecting stdout/stderr (the
[out, err] Promise.all call) keep the stderr/stdout checks and
offRegistryRequests assertion, then assert expect(await exited).toBe(0)
immediately, and only after that call file(join(packageDir, "node_modules",
"no-deps", "package.json")).json() and .toMatchObject; this prevents ENOENT from
masking process failures by ensuring exited is successful before inspecting
node_modules.

In `@test/cli/install/migration/migrate.test.ts`:
- Around line 213-218: Move the assertion of exitCode to immediately after the
stderr checks to fail fast before any filesystem reads: after computing err from
stderr (variable err / stderr.toString()) and the two expect(err)... assertions,
add expect(exitCode).toBe(0) and only then perform the reads/assertions that
touch node_modules and bun.lock (the Bun.file(...) json read and
fs.existsSync(...) checks); this ensures the exitCode is validated before calls
to Bun.file, join(testDir,...), and fs.existsSync.

In `@test/cli/install/symlink-path-traversal.test.ts`:
- Around line 231-261: Move the install success check to run immediately after
asserting stdout/stderr (i.e., after the expect(stderr).toContain(...) and
before iterating the filesystem); specifically, check exitCode (using the
exitCode variable and the existing guard that logs stdout/stderr) and
expect(exitCode).toBe(0) before calling readdir/installDir traversal, readlink,
or access; this prevents readdir/readlink/access (and the escapingSymlinks /
pkgDir checks) from throwing ENOENT and masking the real failure.

In `@test/js/bun/shell/commands/rm.test.ts`:
- Around line 207-223: The test currently launches the delete process into
"running" with $`rm -rf ${target}`.nothrow().quiet().run() but never asserts it
succeeded, allowing silent failures to produce false-positive victim-file
assertions; update the test to await running, capture its result (e.g., the
process/result returned by run()), and add an assertion that the delete command
exited successfully (check result.exitCode === 0 or result.ok depending on the
run() API) before asserting victimFile/victimDir existence so any rm failure
fails the test; reference the running variable and the $`rm -rf
${target}`.nothrow().quiet().run() invocation.

In `@test/js/bun/util/filesystem_router.test.ts`:
- Line 694: This test file contains a per-test explicit timeout expressed as the
trailing "}, 30_000);" on a test invocation; remove the explicit timeout
argument so the test ends with "});" instead and rely on the suite/global
timeouts (do not add any other per-test timeout in test/js/bun/** except the
documented exceptions like ffi/cc.test.ts and glob/leak.test.ts).

In `@test/js/node/fs/fs.test.ts`:
- Line 4063: Replace the test's repetitive string creation for the "data.bin"
entry: instead of using "a".repeat(65536), allocate a buffer and convert to
string (e.g. Buffer.alloc(65536, 'a').toString()) where the "data.bin" value is
set so tests follow the repo guideline for repetitive content.

In `@test/js/node/http/node-http.test.ts`:
- Around line 1864-1866: The test currently asserts exitCode without surfacing
stderr on failure; after the Promise.all that assigns stdout, stderr, exitCode
(from proc.stdout.text(), proc.stderr.text(), proc.exited) insert a guard: if
(exitCode !== 0) { expect(stderr).toBe(""); } immediately before the existing
expect(exitCode).toBe(0) so failing subprocesses print stderr; apply the same
change to the other occurrence that also reads stdout/stderr/exitCode.

In `@test/js/node/http2/node-http2-continuation.test.ts`:
- Line 468: Replace the repetitive-string generation for the "x-filler" header
payload to use Buffer.alloc for performance in debug builds: locate the header
assignment that uses "A".repeat(valueLength) (the "x-filler" key and the
valueLength variable) and change it to use Buffer.alloc(valueLength,
'A').toString() so the test follows the Buffer.alloc(...).toString() convention.

In `@test/js/node/vm/vm.test.ts`:
- Around line 891-903: The test currently assumes calling proto[name] or the
"identifier" getter will fail with a TypeError, but if the property is missing
the .call invocation itself throws and masks the real issue; update the loop and
getter check to first verify the native members exist and are callable: for the
list of method names ensure typeof proto[name] === "function" before invoking
proto[name].call(fake) (and record a clear failure/result if it's missing or not
callable), and for "identifier" use Object.getOwnPropertyDescriptor(proto,
"identifier") to confirm a .get exists before invoking descriptor.get.call(fake)
(again record missing getter if absent) so TypeError is only treated as success
when a valid member was present and the receiver check actually threw.

In `@test/js/node/zlib/zlib.test.js`:
- Around line 639-687: The test may throw before the final deflate.close(),
leaking the native handle; ensure the Deflate instance is always closed by
creating deflate before a try block and moving the existing test logic into try
{ ... } and calling deflate.close() in a finally block (use the existing deflate
variable and handle references so the finally can always call deflate.close()
even if an assertion or await promise throws).

In `@test/js/sql/sql-mysql-raw-length-prefix.test.ts`:
- Line 305: The test builds a long repeated string using "x".repeat(...) for the
bio251 variable; replace that with Buffer.alloc(...) per the test-performance
rule: keep the "\x05admin" prefix and construct the repeated 'x' sequence with
Buffer.alloc(251 - 6, 'x').toString() so bio251 equals "\x05admin" +
Buffer.alloc(251 - 6, 'x').toString(); update the initialization of bio251
accordingly.

In `@test/js/sql/sql-mysql.test.ts`:
- Around line 1086-1176: Extract the entire test case named "MySQL TLS handshake
rejects plaintext packets buffered behind the server greeting" (including helper
functions u16le/u24le/u32le/packet, constants, forgedOk, server setup and the
try/finally block) out of test/js/sql/sql-mysql.test.ts and paste it into a new
file matching the non-Docker pattern (e.g. test/js/sql/sql-mysql-mock.test.ts);
remove the original block from sql-mysql.test.ts so Docker-gated suite no longer
contains mock-socket tests; ensure the new file imports the same
helpers/fixtures (net, mock, using, SQL, expect) that the test needs and runs
under the existing test runner configuration.

In `@test/js/sql/sql.test.ts`:
- Around line 12566-12572: Add an assertion that the spawned fixture's stderr is
empty (after filtering known ASAN startup noise) before checking exitCode so
stderr-only regressions fail the test; specifically, capture proc.stderr.text()
into stderr (already present), apply the same ASAN-noise filtering used in other
bun tests, assert the filtered stderr is empty (or matches the expected
filtered-stderr helper) and place that assertion before the
expect(exitCode).toBe(0) check so any unexpected child diagnostics are caught.

In `@test/js/web/fetch/blob.test.ts`:
- Line 356: The test builds a long repetitive string using "A".repeat(64) when
assigning to the expected variable; replace that with creating the repeated A
sequence via Buffer.alloc and converting to string (i.e., use
Buffer.alloc(count, fill).toString()) and then append "tail" so the assignment
to expected uses Buffer.alloc(64, 'A').toString() + "tail" instead of repeat();
update the same assignment in blob.test.ts where expected is declared.
🪄 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: 0092c02d-507d-4330-961c-f1b25b9f5b75

📥 Commits

Reviewing files that changed from the base of the PR and between 0ddf9df and f4f241c.

📒 Files selected for processing (38)
  • test/bundler/esbuild/packagejson.test.ts
  • test/cli/install/bun-install-registry.test.ts
  • test/cli/install/bun-install.test.ts
  • test/cli/install/bun-lock.test.ts
  • test/cli/install/bun-lockb.test.ts
  • test/cli/install/migration/migrate.test.ts
  • test/cli/install/symlink-path-traversal.test.ts
  • test/js/bun/crypto/cipheriv-decipheriv.test.ts
  • test/js/bun/http/bun-server.test.ts
  • test/js/bun/http/proxy.test.ts
  • test/js/bun/http/serve-pending-promise-abort-leak.test.ts
  • test/js/bun/http/serve.test.ts
  • test/js/bun/jsc/bun-jsc.test.ts
  • test/js/bun/shell/bunshell.test.ts
  • test/js/bun/shell/commands/rm.test.ts
  • test/js/bun/shell/commands/seq.test.ts
  • test/js/bun/sqlite/sqlite.test.js
  • test/js/bun/util/filesystem_router.test.ts
  • test/js/node/crypto/sign-jwk-ieee-p1363.test.ts
  • test/js/node/fs/fs.test.ts
  • test/js/node/http/node-http-parser.test.ts
  • test/js/node/http/node-http.test.ts
  • test/js/node/http2/node-http2-continuation.test.ts
  • test/js/node/http2/node-http2.test.js
  • test/js/node/module/sourcemap.test.js
  • test/js/node/vm/vm.test.ts
  • test/js/node/watch/fs.watch.test.ts
  • test/js/node/zlib/zlib.test.js
  • test/js/sql/sql-mysql-raw-length-prefix.test.ts
  • test/js/sql/sql-mysql.test.ts
  • test/js/sql/sql.test.ts
  • test/js/web/fetch/blob.test.ts
  • test/js/web/fetch/fetch-keepalive.test.ts
  • test/js/web/fetch/fetch.test.ts
  • test/js/web/fetch/fetch.tls.test.ts
  • test/js/web/html/FormData.test.ts
  • test/js/web/timers/setTimeout.test.js
  • test/js/web/websocket/websocket-permessage-deflate.test.ts

Comment thread test/cli/install/bun-lock.test.ts Outdated
Comment thread test/cli/install/migration/migrate.test.ts Outdated
Comment thread test/cli/install/symlink-path-traversal.test.ts Outdated
Comment thread test/js/bun/shell/commands/rm.test.ts
Comment thread test/js/bun/util/filesystem_router.test.ts Outdated
Comment thread test/js/node/zlib/zlib.test.js
Comment thread test/js/sql/sql-mysql-raw-length-prefix.test.ts Outdated
Comment thread test/js/sql/sql-mysql.test.ts Outdated
Comment thread test/js/sql/sql.test.ts
Comment thread test/js/web/fetch/blob.test.ts Outdated
Comment thread src/http/lib.rs
Comment thread src/http/lib.rs Outdated

@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/NativeZlib.rs (1)

149-183: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Validate dictionary before unwrapping as_array_buffer
In NativeZlib::init (src/runtime/node/zlib/NativeZlib.rs:182), arguments.ptr[6] is only checked for undefined; if it’s any non-ArrayBuffer value, arguments.ptr[6].as_array_buffer(global).unwrap() will panic instead of throwing a JS type error. Replace the unwrap with a Nonethrow_invalid_argument_type_value("dictionary", "ArrayBuffer", arguments.ptr[6]) path.

🤖 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/NativeZlib.rs` around lines 149 - 183, The code in
NativeZlib::init currently only checks if arguments.ptr[6] is undefined then
unconditionally calls as_array_buffer(global).unwrap(), which can panic for
non-ArrayBuffer values; change the logic that builds dictionary/dictionary_buf
so that if arguments.ptr[6] is undefined you set dictionary = None, otherwise
call arguments.ptr[6].as_array_buffer(global) and if that returns None return
Err(global.throw_invalid_argument_type_value("dictionary", "ArrayBuffer",
arguments.ptr[6])) instead of unwrap, and only on success bind dictionary_buf
and set dictionary = Some(dictionary_buf.byte_slice()); keep references named
dictionary_buf and dictionary so the borrow lifetime remains valid for the
subsequent stream.init call.
♻️ Duplicate comments (1)
src/http/lib.rs (1)

4254-4267: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ignore Transfer-Encoding for successful CONNECT too.

This only fixes half of the tunnel-framing case. A 200 CONNECT response must ignore Transfer-Encoding as well as Content-Length; otherwise the later Transfer-Encoding branch can still reject or mutate a successful tunnel setup.

Suggested fix
+        let ignore_connect_framing =
+            self.flags.proxy_tunneling && self.proxy_tunnel.is_none() && response.status_code == 200;
         for (header_i, header) in response.headers.list.iter().enumerate() {
             match hash_header_name(header.name()) {
                 h if h == hash_header_const(b"Content-Length") => {
-                    if self.flags.proxy_tunneling
-                        && self.proxy_tunnel.is_none()
-                        && response.status_code == 200
-                    {
+                    if ignore_connect_framing {
                         continue;
                     }
                     let Ok(content_length) = bun_core::parse_unsigned::<usize>(header.value(), 10)
                     else {
                         return Err(err!(InvalidHTTPResponse));
@@
                 }
                 h if h == hash_header_const(b"Transfer-Encoding") => {
+                    if ignore_connect_framing {
+                        continue;
+                    }
                     if header.value() == b"gzip" {
🤖 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/http/lib.rs` around lines 4254 - 4267, The CONNECT success case currently
ignores only Content-Length; extend the same logic to also ignore
Transfer-Encoding for 200 CONNECT responses so the tunnel framing code can't be
rejected or mutated later. In the header match handling (see the arm using
hash_header_const(b"Content-Length")), apply the same conditional check
(self.flags.proxy_tunneling && self.proxy_tunnel.is_none() &&
response.status_code == 200) to the Transfer-Encoding header branch (the branch
that matches hash_header_const(b"Transfer-Encoding") or the later
Transfer-Encoding handling) so that Transfer-Encoding is likewise skipped for
successful CONNECT responses.
🤖 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/zlib/NativeBrotli.rs`:
- Around line 200-215: The init path in NativeBrotli is failing to validate
arguments.ptr[0] ("params") before calling as_u32(), which can panic or
misinterpret non-Uint32Array inputs; update NativeBrotli::init to mirror the
writeResult checks: call arguments.ptr[0].as_array_buffer(global_this) and
return Err(global_this.throw_invalid_argument_type_value("params",
"Uint32Array", params_value)) if it fails, then verify
params_buf.typed_array_type == bun_jsc::JSType::Uint32Array and return the same
throw_invalid_argument_type_value on mismatch, and only then call
params_buf.as_u32() to obtain the params slice.

---

Outside diff comments:
In `@src/runtime/node/zlib/NativeZlib.rs`:
- Around line 149-183: The code in NativeZlib::init currently only checks if
arguments.ptr[6] is undefined then unconditionally calls
as_array_buffer(global).unwrap(), which can panic for non-ArrayBuffer values;
change the logic that builds dictionary/dictionary_buf so that if
arguments.ptr[6] is undefined you set dictionary = None, otherwise call
arguments.ptr[6].as_array_buffer(global) and if that returns None return
Err(global.throw_invalid_argument_type_value("dictionary", "ArrayBuffer",
arguments.ptr[6])) instead of unwrap, and only on success bind dictionary_buf
and set dictionary = Some(dictionary_buf.byte_slice()); keep references named
dictionary_buf and dictionary so the borrow lifetime remains valid for the
subsequent stream.init call.

---

Duplicate comments:
In `@src/http/lib.rs`:
- Around line 4254-4267: The CONNECT success case currently ignores only
Content-Length; extend the same logic to also ignore Transfer-Encoding for 200
CONNECT responses so the tunnel framing code can't be rejected or mutated later.
In the header match handling (see the arm using
hash_header_const(b"Content-Length")), apply the same conditional check
(self.flags.proxy_tunneling && self.proxy_tunnel.is_none() &&
response.status_code == 200) to the Transfer-Encoding header branch (the branch
that matches hash_header_const(b"Transfer-Encoding") or the later
Transfer-Encoding handling) so that Transfer-Encoding is likewise skipped for
successful CONNECT responses.
🪄 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: efc7af89-4443-4af4-ad46-bb93b1483fa3

📥 Commits

Reviewing files that changed from the base of the PR and between 4eddd97 and e262b5e.

📒 Files selected for processing (25)
  • src/http/lib.rs
  • src/http_types/URLPath.rs
  • src/install/lockfile/Tree.rs
  • src/install/lockfile/bun.lock.rs
  • src/install/migration.rs
  • src/js/internal/sql/postgres.ts
  • src/runtime/node/win_watcher.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • src/runtime/node/zlib/NativeZlib.rs
  • src/runtime/server/RequestContext.rs
  • src/url/lib.rs
  • test/cli/install/bun-lock.test.ts
  • test/cli/install/migration/migrate.test.ts
  • test/cli/install/symlink-path-traversal.test.ts
  • test/js/bun/util/filesystem_router.test.ts
  • test/js/node/fs/fs.test.ts
  • test/js/node/http/node-http.test.ts
  • test/js/node/http2/node-http2-continuation.test.ts
  • test/js/node/vm/vm.test.ts
  • test/js/node/zlib/zlib.test.js
  • test/js/sql/sql-mysql-raw-length-prefix.test.ts
  • test/js/sql/sql-mysql-tls-plaintext-injection.test.ts
  • test/js/sql/sql-mysql.test.ts
  • test/js/sql/sql.test.ts
  • test/js/web/fetch/blob.test.ts
💤 Files with no reviewable changes (1)
  • test/js/sql/sql-mysql.test.ts

Comment thread src/runtime/node/zlib/NativeBrotli.rs
Comment thread src/http/lib.rs
Comment thread src/jsc/bindings/node/http/NodeHTTPParser.cpp Outdated
@Jarred-Sumner Jarred-Sumner force-pushed the claude/hardening-sweep-3 branch from 54fb966 to a24abe1 Compare May 23, 2026 00:23
Comment thread test/js/web/websocket/websocket-permessage-deflate.test.ts Outdated
Comment thread src/http/lib.rs Outdated
Comment thread src/runtime/node/types.rs Outdated
@Jarred-Sumner Jarred-Sumner force-pushed the claude/hardening-sweep-3 branch from f7d9488 to e6aaecb Compare May 23, 2026 02:42
Comment thread src/js/node/_http_client.ts Outdated
Comment thread src/uws/lib.rs Outdated
Comment thread src/http/lib.rs
Comment thread src/js/node/wasi.ts
Comment thread test/js/bun/http/bun-server.test.ts
Comment thread src/jsc/bindings/sqlite/JSSQLStatement.cpp Outdated
Comment thread src/runtime/node/node_zlib_binding.rs Outdated
Comment thread test/js/bun/http/serve.test.ts
Comment thread src/runtime/node/types.rs Outdated
Comment thread src/runtime/node/node_fs.rs
Comment thread src/jsc/bindings/bindings.cpp
Comment thread test/js/web/fetch/blob.test.ts
Comment thread test/js/node/vm/vm.test.ts

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

No new issues found on this pass — all prior inline findings have been addressed. That said, this is a 109-file security-hardening sweep touching TLS/SNI pooling, crypto, deserialization bounds, install path traversal, and SQL/WASI input validation, with several subtle interactions already caught and fixed during review (pin/unpin balance, h2/h3 redirect hostname clear, throw-scope validator), so it warrants a human pass before merge. Also note Build #57268 still shows a build-rust failure on 🍎 x64.

Extended reasoning...

Overview

This PR is a 56-commit, 109-file input-validation and bounds-tightening sweep across 31 subsystems: TLS socket pooling and Host/SNI override keying (HTTPContext.rs, http/lib.rs), HTTP/H2 parser dispatch and header validation, permessage-deflate payload caps, JSC receiver checks and SerializedScriptValue deserialization bounds, ArrayBuffer pin/unpin for async zlib/fs I/O, install/lockfile folder-name and symlink-target validation, SQL transaction-option and array-type-name validation, MySQL packet framing and TLS handshake-tail checks, node:crypto authTagLength and P1363 conversion, shell rm NOFOLLOW and seq termination, WASI preopen containment, and more — each with new regression tests.

Security risks

The PR is itself the security surface: it changes TLS certificate-verification keying across redirects, crypto signature-encoding fallback, path-traversal guards in the installer, SQL string interpolation validation, structured-clone deserialization bounds, and WASI sandbox containment. Several review rounds already caught regressions in these exact areas (cross-origin redirect mis-pooling under proxy_auth_hash=0, h2/h3 not consuming clear_hostname_on_redirect, leaked ArrayBuffer pins on every async zlib write / fs.read, leaked pins on the position validation error path, throw-scope validator failures on x64-asan). All were fixed in follow-up commits, but the density of subtle cross-cutting interactions is high.

Level of scrutiny

High. This is production-critical, security-sensitive code across the HTTP client, TLS layer, package installer, crypto bindings, and JSC FFI boundary. The changes are individually small and well-commented, but the review history (12+ rounds of inline findings, multiple of which were genuine regressions introduced by earlier commits in the same PR) demonstrates that the interactions are non-obvious. This is well outside the "simple/mechanical" bar for auto-approval.

Other factors

  • All 30+ prior inline comments (CodeRabbit and claude) are resolved, and the author confirmed sweeping the remaining stderr-assertion nits.
  • The bug-hunting system found no new issues on this final pass.
  • One open DerivedArray Node-compat narrowing note (types.rs:1433) was flagged as intentional by design and left for the author to confirm.
  • CI: the latest robobun update for commit 2d6811d still reports 1 failure (scripts/build/ci.ts build-rust on 🍎 x64), which should be green before merge.

Adds JSValue::as_pinned_arraybuffer() and uses the generated cached-value
slots on the zlib handle classes to keep in-flight write buffers reachable,
replacing the per-handle Strong pair and the separate unpin path. fs.read
reuses the same accessor. writev/readv now require a plain dense array.
A re-entrant property getter for a later parameter can run before
sqlite3_step(), so a statically-bound string pointer can outlive its
backing store. Bind text the same way blobs are now bound and drop the
unused clone flag from the rebind chain.
Collect the elements of a writev/readv array before taking any data
pointer, using the same MarkedArgumentBuffer pattern as Buffer.concat,
and for the async path root each element and pin its backing store until
the operation completes. SharedArrayBuffer-backed views are left
unpinned everywhere since they can never be detached.
The buffer-span collector declares a throw scope, so its Rust caller has
to discharge the simulated throw the way every other throwing binding
call does. Release the collected buffers when a later argument fails
validation, and unprotect the parsed arguments on the defensive early
returns. Assert empty stderr in two subprocess tests that captured it
without checking it.
@Jarred-Sumner Jarred-Sumner force-pushed the claude/hardening-sweep-3 branch from 4ee8320 to 749270b Compare May 23, 2026 10:45
Comment thread packages/bun-usockets/src/crypto/openssl.c
Comment thread src/runtime/webcore/FormData.rs
Jarred-Sumner and others added 2 commits May 23, 2026 11:55
An interior tab is valid optional whitespace in a field value and cannot
start a new header line, so the printable-ASCII guard should not drop
content types that contain one. Also keep the renegotiation window from
resetting when the wall clock steps backwards, and back the 2GB write
regression test with 256 shared chunks so its peak RSS fits the CI
runners.
Comment thread test/regression/issue/8254.test.ts
Comment thread src/runtime/node/node_zlib_binding.rs Outdated
Pinning a write buffer can fail when the backing store cannot be
materialized; throw instead of panicking, and do it before any write
state is mutated so there is nothing to unwind.
Comment thread test/js/node/http2/node-http2.test.js Outdated
@Jarred-Sumner Jarred-Sumner merged commit a8aec30 into main May 24, 2026
74 of 75 checks passed
@Jarred-Sumner Jarred-Sumner deleted the claude/hardening-sweep-3 branch May 24, 2026 02:38
Jarred-Sumner pushed a commit that referenced this pull request Jun 1, 2026
…y name (#31655)

Closes #31652

## Repro

`bun install -g @openai/codex` on `1.4.0-canary` aborts with:

```
error: Invalid dependency name ""
error: Invalid dependency name ""
error: Invalid dependency name ""
```

On `1.3.x` the same command installs fine. Minimal reduction — a package
whose manifest declares an **optional** dependency under an empty key
whose target doesn't resolve on the current platform:

```jsonc
// manifest for top@1.0.0
{ "optionalDependencies": { "": "1.0.0" } }   // target "" 404s / is platform-filtered
```

```
$ bun install          # 1.3.14 → exit 0, optional dep skipped
$ bun install          # 1.4.0-canary → error: Invalid dependency name "", exit 1
```

## Cause

When a dependency alias (the key) is empty, the real package name is
only substituted back into `Dependency.name` **if the dependency
resolves** (`assign_resolution` / `assign_root_resolution` in
`PackageManagerResolution.rs`). An *optional* dependency that fails to
resolve keeps `pkg_id == invalid_package_id` and an **empty** name.

That empty-named dependency then reaches the hoisting tree builder,
where a name-safety check added in #31221
(`src/install/lockfile/Tree.rs`) treats the empty name as unsafe and
calls `maybe_report_error("Invalid dependency name \"\"")`, aborting the
install. The original Zig implementation had no such check here, which
is why 1.3.x works.

The check was also added **inconsistently**: the two sibling call sites
already guard the empty case —

- `src/install/lockfile/bun.lock.rs`: `if !name_str.is_empty() &&
!is_safe_install_folder_name(name_str)`
- `src/install/isolated_install.rs`: `if !name.is_empty() &&
!is_safe_install_folder_name(name)`

— while `Tree.rs` was the only one missing `!name.is_empty()`.

## Fix

Guard `!name.is_empty()` before the safety check in `Tree.rs`, matching
the sibling call sites. An empty alias has no `node_modules/<name>`
folder to escape, so it is skipped like an unresolved optional dep
should be, instead of failing the whole install. Genuinely unsafe
*non-empty* names (e.g. `..`, `../..`) still error exactly as before.

## Verification

`test/regression/issue/31652.test.ts` serves a package with
`optionalDependencies: { "": "1.0.0" }` whose target 404s and asserts
the install completes without `Invalid dependency name ""` and with exit
0.

- Fails on `main`: `error: Invalid dependency name ""`, exit 1.
- Passes with this change.
- The existing alias-path-traversal rejection tests (`rejects dependency
aliases containing '..' path segments`, `does not create a cache index
entry outside the cache directory for a dependency alias of '..'`, and
the two sibling `'..'` tests) still pass — the empty-name guard does not
weaken the security checks.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
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