Skip to content

Hardening: input validation and protocol tightening across 24 subsystems (round 7)#31495

Merged
Jarred-Sumner merged 69 commits into
mainfrom
claude/security-round-7
May 28, 2026
Merged

Hardening: input validation and protocol tightening across 24 subsystems (round 7)#31495
Jarred-Sumner merged 69 commits into
mainfrom
claude/security-round-7

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Tightens input validation, bounds checking, protocol framing, and state handling across the package manager, HTTP stack, sockets/TLS, SQL clients, and parsers. Continuation of #31417 (round 6); same structure — small per-area commits with regression tests included.

Package manager / CLI

  • install: validate extract folder names and dependency aliases for the remaining resolution shapes; bounds-check resolution lookups during lockfile loads
  • install: trusted-dependency entries for non-npm resolutions only apply when the dependency is declared by the root or a workspace; the security scanner is also told about packages reached through non-npm parents
  • install: registry auth tokens stay scoped to their configured host (and are not carried across an https→http redirect)
  • pm: bun pm cache rm resolves the cache directory from the process environment, ignoring project-local overrides for the destructive delete (bun pm cache prints the same path it would delete)
  • pack: validate the name/version used to form the tarball filename
  • compile: build artifacts are created exclusively with owner-only modes; repl: the history file is created 0600 (and an existing one is tightened)
  • yarn.lock migration behavior is unchanged; a comment now documents the intentionally lenient handling of entries without integrity

HTTP / HTTP2 / WebSocket / S3

  • fetch: never forwards a caller-supplied Content-Length that disagrees with the actual body (node:http behavior unchanged); drains interim 1xx bytes as they are parsed; never returns a connection with a framing violation to the keep-alive pool
  • node:http2: rejects requests carrying connection-specific headers, repeated single-value/pseudo headers, or content-length/DATA mismatches (verified against Node/nghttp2 behavior); copies queued DATA frames before re-entrant writes; keeps parsing safe when a socket chunk's buffer is transferred mid-frame
  • Bun.serve: absolute-form request targets are routed by path with request.url derived from Host; no message-body bytes are written for null-body statuses (101/103/204/205/304) — Response construction is unchanged
  • websocket: handshake buffering survives client teardown from the open handler
  • S3: object keys keep ?/# characters end-to-end; list-response parsing is linear; the remote-address accessor copies into caller-owned storage

Sockets / TLS / SQL / runtime

  • sql: an explicit tls/ssl option aborts the connection before credentials are sent if the server declines TLS; certificate-verification behavior is unchanged. Row construction is bounded by the declared structure; MySQL rebuilds cached row shapes when result columns change; postgres protocol counts are validated against the received bytes
  • node:tls: server sockets only report authorized: true after a client certificate was requested and verified (matches observed Node behavior)
  • sockets: handler reload and accepted-socket reuse re-validate ownership before freeing; the TLS read path restores shared dispatch state after re-entrant callbacks; pipe reads re-acquire the scratch buffer per use
  • sqlite: the cross-worker database registry and lazy library load are lock-protected; spawned children close descriptors across the full fd range
  • crypto: encoded-hash cost parameters are bounded before verification; errors: stack-frame callees stay GC-reachable while formatting

Parsers / shell / node

  • markdown: reference-definition lookup, table column counts, and autolink scanning are linear/bounded
  • YAML: alias expansion is budgeted in the parser used by imports and the bundler
  • node:fs: watcher path joins surface ENAMETOOLONG instead of aborting; assert: diff output is bounded for large operands

Tests

33 new regression tests across the touched areas (validated to fail on the released build and pass here), plus a handful of regression-only cases where the released build does not exhibit the behavior deterministically. A few fixes have no standalone test where the change is not observable from JS.

cargo check and the cross-target checks pass on all 10 CI targets; the touched suites pass locally with no new failures versus main. The branch is rebased on current main.

Changes tried and reverted during review (rationale)

  • Lockfile lifecycle scripts — an earlier commit re-derived dependency lifecycle script text from each installed package's package.json instead of the deserialized lockfile data; reverted because bun.lockb intentionally stores and uses the script text, and that design stays.
  • yarn.lock migration — an earlier commit rejected off-registry tarball URLs without integrity during migration; reverted because yarn v1 lockfiles legitimately contain such entries (workspace, file:, codeload) and migration must not fail closed. The lenient handling is now documented with a comment at the site.
  • Response and null-body statuses — both the body-dropping and the later spec-style TypeError for non-null bodies with 101/103/204/205/304 were reverted: existing code and frameworks (including Elysia) construct such responses and expect the body to be ignored, so changing construction is a breaking change nobody asked for. Only the wire-level behavior (no body bytes emitted) is kept.
  • SQL client TLS — an earlier commit made an explicit tls/ssl option imply certificate verification; narrowed to abort-on-decline only because the unverified default for these options is intentional and matches other JavaScript database clients.
  • node:http path with an absolute URI — an earlier commit rewrote such requests to a path-only target; removed because measured Node behavior does not do that, and Node compatibility takes precedence.

@robobun

robobun commented May 28, 2026

Copy link
Copy Markdown
Collaborator
Updated 5:19 PM PT - May 28th, 2026

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


🧪   To try this PR locally:

bunx bun-pr 31495

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

bun-31495 --bun

@coderabbitai

coderabbitai Bot commented May 28, 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 hardens security and protocol robustness across network, package manager, database, and parsing layers. Changes include TLS read-state preservation during dispatch, owned SocketAddress types, absolute-form URL routing normalization, HTTP/2 connection-specific header rejection, YAML/XML/Markdown parsing bounds enforcement, SQLite thread-safe handle registry, lockfile trust verification for non-npm resolutions, tarball and registry token validation, and comprehensive regression tests covering all changes.

Changes

Network Stack Security: TLS, Socket Lifecycle, URL Routing, HTTP Protocol and Parsing

Layer / File(s) Summary
TLS Read State Preservation and Socket Handler Lifecycle
packages/bun-usockets/src/crypto/openssl.c, src/http/HTTPContext.rs, src/http_jsc/websocket_client.rs, src/runtime/socket/*
Saves/restores TLS BIO input state across us_dispatch_data to prevent reentrancy corruption; tightens handler reuse cleanup to require both OWNS_HANDLERS flag and zero active connections; detaches websocket initial-data handler on deinit instead of dropping to allow microtask cleanup; validates socket.reload handler pointer liveness before state update.
Absolute-Form URL Extraction and Owned Socket Address
packages/bun-uws/src/HttpContext.h, packages/bun-uws/src/HttpParser.h, src/runtime/webcore/Request.rs, src/runtime/bake/DevServer.rs, src/runtime/server/RequestContext.rs, src/runtime/bake/dev_server/mod.rs, src/uws_sys/Response.rs, src/uws_sys/h3.rs, src/uws/lib.rs
HTTP routing normalized via getUrlForRouting() stripping scheme/authority from absolute-form requests; new request_target_path helper handles both absolute and origin-form URLs for routing; SocketAddress redesigned from lifetime-borrowing to owned fixed-capacity IP storage (64 bytes) across all call sites; response status 101/103/204/205/304 short-circuit with do_render_blob().
HTTP Response Parsing, Keep-Alive Control, and Content-Length
src/http/lib.rs, src/io/PipeReader.rs, src/runtime/webcore/fetch.rs, src/runtime/webcore/fetch/FetchTasklet.rs
Short-read handling aligned to current read window; 1xx response buffer draining before continuation; Content-Length overshoot disables keep-alive to prevent pipelined reuse; PipeReader eliminates persistent buffer borrows across callbacks; conditional Content-Length emission for Node HTTP client when original or zero-value present.
HTTP/2 Header Validation and Frame Flushing
src/runtime/api/bun/h2_frame_parser.rs
New forbidden-connection-specific header helper with trailers exception; single-value header tracking for pseudo-headers and content-length with malformed rejection; pseudo-header ordering verification; owned-frame-based flush queue refactoring with proper ownership through flow control and padding.
Parsing Bounds and Resilience: XML, YAML, Markdown
src/runtime/webcore/s3/list_objects.rs, src/parsers/yaml.rs, src/md/line_analysis.rs, src/md/links.rs, src/md/ref_defs.rs
S3 XML parser early-exit on missing closing tags; YAML alias-expansion budget cap with per-node charge preventing exponential materialization; markdown table column-count cap at 128; URI autolink termination on < character; reference definition lookup refactored from O(n) scan to indexed access.
Error Rendering and Diff Memory Bounds
src/js/internal/assert/assertion_error.ts, src/runtime/node/assert/myers_diff.rs
Myers diff trace capped at 64 MiB; colored diff wrapped with stacked-diff fallback; error messages truncated with bounded line count and ellipsis fallback.
TLS Authorization, SQL Cache, and Miscellaneous
src/js/node/_http2_upgrade.ts, src/js/node/net.ts, src/jsc/bindings/FormatStackTraceForJS.cpp, src/sql_jsc/mysql/MySQLConnection.rs, src/jsc/bindings/SQLClient.cpp, src/jsc/bindings/bun-spawn.cpp, src/js/internal/sql/shared.ts, src/runtime/crypto/pwhash.rs
socket.authorized only true when _requestCert enabled (mutual TLS); stack trace OOM check with MarkedArgumentBuffer overflow; MySQL column cache reset and change detection; SQL structure offset validation; spawn maxfd increased to 65536; PostgreSQL TLS mode require (not prefer); Argon2 parameter verification bounds.

Package Manager Security: Trust Scoping, Tarball Validation, Registry Control

Layer / File(s) Summary
Lockfile Trust Verification and Root/Workspace Declaration
src/install/lockfile.rs
Trusted-dependency lookup keyed by resolution tag; npm resolutions checked directly with early return; non-npm resolutions delegated to new declared_by_root_or_workspace helper scanning Root/Workspace packages for matching dependency and resolution equality.
Security Scanner Package Collection Deferral
src/install/PackageManager/security_scanner.rs
Removed npm-only pre-filtering from collection loops; deferred insertion into package_paths until process_queue when Tag::Npm verified; packages_scanned derived from package_paths count; updated safety comments.
Tarball Safety Validation and Registry Token Scoping
src/install/TarballStream.rs, src/install/extract_tarball.rs, src/install/PackageManager/PackageManagerOptions.rs, src/install/yarn.rs
Truncated-basename (32 bytes) with safe-install-folder validation; fallback to fixed package suffix for git/local tarballs; explicit error for invalid non-git/non-local names; registry token preserved only on host+scheme match; yarn off-registry tarballs allowed without integrity.
Pack Command Validation and Cache Directory Resolution
src/runtime/cli/pack_command.rs, src/runtime/cli/package_manager_command.rs
pack() validates name/version post-lifecycle for empty/unsafe-component rejection; bun pm cache rm uses dotenv-loaded environment for cache directory via fetch_cache_directory_path, respects BUN_INSTALL hierarchy, ignores project .env overrides.

SQLite Thread Safety and Handle Registry

Layer / File(s) Summary
SQLite Database Handle Registry with Global Lock
src/jsc/bindings/sqlite/JSSQLStatement.cpp, src/jsc/bindings/sqlite/lazy_sqlite3.h
WTF::Lock protection for handle registry; registerDatabase() and databaseForHandle() helpers validate handles with RangeError on invalid; all API functions (serialize, loadExtension, execute, prepare, close, fcntl, isInTransaction) use validated handles; db-pointer nullification on close marks handle as closed; finalizer releases via stored pointer instead of captured index.

File Permissions, Filesystem Bounds, and Credential Security

Layer / File(s) Summary
Standalone Permissions, fs.watch Path Bounds, REPL History, and URL Handling
src/standalone_graph/StandaloneModuleGraph.rs, src/runtime/node/node_fs_watcher.rs, src/runtime/cli/repl.rs, src/url/lib.rs, src/js/internal/sql/shared.ts
Temp file mode 0o700 (owner-only) before cleanup; O_EXCL flag for exclusive creation; post-injection chmod to 0o755; fs.watch uses join_abs_string_buf_checked with ENAMETOOLONG on overflow; REPL history created with 0o600 plus post-hoc fchmod enforcement; URL s3_path returns normalized path portion.

Comprehensive Regression Test Coverage

Layer / File(s) Summary
Package Manager Security and Trust Tests
test/cli/install/bun-install-lifecycle-scripts.test.ts, test/cli/install/bun-install-registry.test.ts, test/cli/install/bun-install.test.ts, test/cli/install/bun-pack.test.ts, test/cli/install/bun-pm.test.ts
Transitive tarball trust rejection; registry token host/scheme matching; cache rm ignores .env overrides; malicious alias path-traversal rejection; safe tarball extraction validation.
HTTP Routing, Socket Lifecycle, and TLS Tests
test/js/bun/http/bun-serve-routes.test.ts, test/js/bun/net/socket.test.ts, test/js/bun/http/serve.test.ts, test/js/web/websocket/websocket-client-short-read.test.ts
Absolute-form request routing and Host header handling; TLS 512KiB boundary dispatch with state preservation; socket reload termination safety; node:net handle reuse; WebSocket buffered handshake; null-body status codes (204/205/304).
HTTP/2 Protocol Validation and Fetch Tests
test/js/node/http2/node-http2.test.js, test/js/web/fetch/fetch.test.ts
Malformed HEADERS rejection; client flush stability; ArrayBuffer transfer resilience; fetch bodyless Content-Length; interim 1xx memory bounds; keep-alive disable on overshoot.
Parsing Bounds and Resilience Tests
test/js/bun/s3/s3-list-objects.test.ts, test/js/bun/yaml/yaml.test.ts, test/js/bun/md/md-edge-cases.test.ts
S3 XML malformed content; YAML alias expansion cap; markdown table/autolink pathological inputs; reference definition lookup performance.
Error Rendering, SQL, and Cryptographic Tests
test/js/node/assert/assert-typedarray-deepequal.test.ts, test/js/bun/sqlite/sqlite.test.js, test/js/sql/sql-mysql.test.ts, test/js/sql/sql.test.ts, test/js/sql/tls-sql.test.ts, test/js/bun/util/password.test.ts
Error message size bounds; SQLite concurrent handle registry; MySQL column caching; PostgreSQL short DataRow; PostgreSQL TLS decline; Argon2 parameter tampering.
Filesystem, TLS Authorization, REPL, and S3 Tests
test/js/node/watch/fs.watch.test.ts, test/js/node/tls/node-tls-server.test.ts, test/js/bun/repl/repl.test.ts, test/js/bun/s3/s3-list-encode-overflow.test.ts
fs.watch path overflow ENAMETOOLONG; TLS mutual-certificate authorization; REPL history permission tightening; S3 special-character encoding.

Possibly Related PRs

  • oven-sh/bun#31417: Both PRs harden uSockets TLS state across JS/callback dispatch by adjusting packages/bun-usockets/src/crypto/openssl.c to restore/reapply ssl_set_loop_data / loop_ssl_data after dispatch points.
  • oven-sh/bun#31323: Both PRs modify the HTTP/2 header decoding logic in src/runtime/api/bun/h2_frame_parser.rs to improve header validation and malformed-request rejection.

Suggested Reviewers

  • alii

Comment thread src/runtime/webcore/Request.rs
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Confirmed and fixed in 0a37560. clippy's undocumented_unsafe_blocks only accepts a SAFETY comment on the lines directly above the unsafe block (or above a let/const), so prepending the OWNS_HANDLERS check pushed the unsafe block off the line below the existing comment — at all three sites this change touched (Listener.rs ~1077, ~1177, 1434), not just 1434. Moved the existing SAFETY comments to sit directly above the && unsafe operand inside each condition; verified clean against the pinned nightly clippy and rustfmt. Comment-only change, no behavior change.

Comment thread src/runtime/cli/package_manager_command.rs
Comment thread src/runtime/cli/repl.rs
Comment thread src/js/node/_http_client.ts Outdated
Comment thread src/jsc/bindings/sqlite/JSSQLStatement.cpp
Comment thread src/install/PackageManager/PackageManagerOptions.rs
Comment thread src/install/lockfile/Package.rs Outdated
));
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This isn't the right way to address this - we can keep storing the scripts in the binary lockfile.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reverted in defda3d — bun.lockb keeps storing the lifecycle scripts and the install path loads and uses them exactly as on main; the load-time clearing in Package.rs is gone and the test that asserted that behavior is removed. One question: would you like a follow-up that re-validates the stored script text against the installed package's package.json at execution time, or do you prefer leaving this exactly as on main?

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

r7t4-01: Confirmed and fixed in 9c01179. The Content-Length change in build_request() also applied to node:http's client (it goes through nodeHttpClient → the same request builder), so the conflicting "Content-Length: 1, 2" that test-http-double-content-length.js relies on never reached the wire and the server accepted the request instead of emitting clientError. The request builder now distinguishes the nodeHttpClient entry point via a flag threaded through FetchOptions: the Node client forwards an explicitly set Content-Length on bodyless requests exactly as Node does (and as main did), while fetch() keeps refusing to emit a Content-Length that doesn't describe the body it actually sends. Verified with test/js/node/test/parallel/test-http-double-content-length.js, fetch.test.ts -t "Content-Length", and node-http.test.ts.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

r7t4-02: Reworked as requested in d4184b8: the Response constructor and Response.json() no longer throw for 101/103/204/205/304 — the body is accepted and dropped, so response.body ends up null and Bun.serve never has body bytes to write for those statuses (the on-wire property from the original change is preserved without the breaking TypeError). The regression test in test/js/web/fetch/response.test.ts now asserts ignore semantics (.body === null, text() === ""), the serve.test.ts status-code workaround is reverted back to new Response("hey", { status: +code }), and a raw-socket Bun.serve check asserts nothing follows the header block when a 204/205/304 Response is constructed with a body. This unbreaks Elysia's status.test.ts (new Response('', { status })) and the http3 client "status 204" test.

Comment thread src/runtime/webcore/Response.rs Outdated
Comment thread test/js/bun/s3/s3-list-objects.test.ts Outdated
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Re: the websocket LSan report (r7t6-01) — confirmed and fixed in ca57e25. The LSan report on debian-13-x64-asan (build 58567) is the 32-byte InitialDataHandler box allocated in init() (websocket_client.rs:1832), leaked by the NO_PROXY-bypass subprocesses: those fixtures call process.exit(0) from the open handler while the server's first frame arrived in the same packet as the 101, so the buffered-handshake microtask never drains. After the ownership change in this PR, deinit() only detached the handler and left the free to that microtask — which never runs once the VM is being torn down (the queue is dropped unexecuted), so the box leaked under BUN_DESTRUCT_VM_ON_EXIT.

The fix keeps the detach for a still-running VM (the queued microtask remains the single owner and frees the box, so the mid-tick close path covered by websocket-client-short-read.test.ts stays free of the original double-free) and additionally reclaims the box in deinit() only when the VM is shutting down, i.e. when the microtask can no longer run. NO_PROXY tests pass under the CI leak-check env (BUN_DESTRUCT_VM_ON_EXIT=1, detect_leaks=1) and websocket-client-short-read still passes.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Re: the 28632 RSS regression (r7t6-02) — confirmed and fixed in 6801c8c. The header-time reset made every re-execution of a reused prepared statement drop its cached row shape, so the duplicate-name pass and structure rebuild ran per query and the resulting per-execution alloc/free churn pushes test/regression/issue/28632.test.ts past its ASAN bound (the same churn class that test was added for — see the elision note in ColumnDefinition41.rs). Fixed by detecting changes where they are actually observable: ColumnDefinition41::decode now reports whether the column identifier differs from the previously decoded one, and the connection only resets cached_structure/fields_flags (and re-arms the duplicate check) when a column changed or the column count changed. Same-count renames still invalidate, so the new "rebuilds row object shape when a reused statement's result columns change" coverage in sql-mysql.test.ts keeps passing, while identical re-executions reuse the cached shape as before. Verified locally against the dockerized MySQL: 28632 passes and the full sql-mysql.test.ts shape-rebuild coverage passes.

Comment thread test/js/bun/sqlite/sqlite.test.js

@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

Caution

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

⚠️ Outside diff range comments (3)
src/parsers/yaml.rs (1)

2326-2359: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset the alias budget per document.

parse_stream() reuses one Parser for the whole YAML stream, but this counter is only initialized once here. A large first document can therefore consume budget and make a later document fail with ExcessiveAliasing even though each document stays within the intended per-document limit.

Suggested fix
 fn parse_document(&mut self) -> Result<Document, ParseError> {
     let mut directives: Vec<Directive> = Vec::new();

     self.anchors.clear();
     self.tag_handles.clear();
+    self.alias_expansion_budget = Self::MAX_ALIAS_EXPANSION;
🤖 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/parsers/yaml.rs` around lines 2326 - 2359, The alias expansion budget is
only set once in Parser::init so parse_stream() reusing the Parser across
multiple documents can run out of budget on later documents; update the
start-of-document path (where parse_stream begins processing each new document)
to reset parser.alias_expansion_budget = Parser::MAX_ALIAS_EXPANSION (and
optionally parser.merge_props_budget = MappingProps::MAX_MERGED_PROPERTIES) so
each document gets a fresh per-document budget; locate the document-start code
invoked by parse_stream and add this reset before parsing the document body.
src/runtime/cli/pack_command.rs (1)

2252-2274: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-run the restricted-access scoped-name check after lifecycle scripts.

Line 2254 reloads name, but the Access::Restricted / scoped-package validation from the first-read path is not repeated here. A prepack/prepublishOnly script can change @scope/pkg to pkg, and this branch will keep going with RestrictedUnscopedPackage bypassed.

Suggested fix
         package_name = package_name_expr
             .as_string_cloned(bump)?
             .ok_or(PackError::InvalidPackageName)?;
         if package_name.is_empty() || has_unsafe_tarball_filename_part(package_name) {
             return Err(PackError::InvalidPackageName);
         }
+        if FOR_PUBLISH {
+            let is_scoped = bun_install::dependency::is_scoped_package_name(package_name)
+                .map_err(|_| PackError::InvalidPackageName)?;
+            if let Some(access) = manager.options.publish_config.access {
+                if access == bun_install::Access::Restricted && !is_scoped {
+                    return Err(PackError::RestrictedUnscopedPackage);
+                }
+            }
+        }
🤖 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/cli/pack_command.rs` around lines 2252 - 2274, After re-reading
package_name/version you must re-run the restricted-access scoped-name
validation so lifecycle scripts can't remove a required scope; after the
package_name is reassigned (the block that sets package_name from
json.root.get("name") and validates emptiness), check if access ==
Access::Restricted and if so validate package_name still has a scope and
otherwise return PackError::RestrictedUnscopedPackage (same logic used in the
first-read path). Place this check immediately after the package_name
re-read/validation and mirror the original scoped-name validation routine used
earlier.
src/jsc/bindings/bun-spawn.cpp (1)

41-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t hard-cap the fd fallback sweep at 65536 in bun-spawn

  • getMaxFd() clamps maxfd to 65536, and closeRangeLoop() iterates with fd < maxfd, so 65536 becomes an exclusive upper bound.
  • The cloexec fallback call is closeRangeOrLoop(current_max_fd + 1, INT_MAX, true), and end == INT_MAX bypasses the end + 1 logic—so the loop sweep stops at the 65536 clamp, leaving fds >= 65536 untouched (non-Linux always; Linux only when bun_close_range() fails).
  • Use getrlimit(RLIMIT_NOFILE) (soft limit) to size the sweep, with a small conservative default only when querying it fails.
🤖 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/jsc/bindings/bun-spawn.cpp` around lines 41 - 45, The current fd-sweep
clamps maxfd to 65536 in getMaxFd(), causing closeRangeLoop()/closeRangeOrLoop()
to stop at that exclusive bound and leave fds >= 65536 open; update getMaxFd()
(and any place using the 65536 magic) to query the process soft limit via
getrlimit(RLIMIT_NOFILE) and use that as the sweep upper bound, falling back to
a small conservative default only if getrlimit fails, and preserve the existing
logic that treats end == INT_MAX as “no end” so end+1 isn't applied when calling
closeRangeOrLoop()/closeRangeLoop(); ensure bun_close_range() failure still
triggers the loop fallback that uses the getrlimit-derived bound.
🤖 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/install/lockfile.rs`:
- Around line 3413-3418: The code indexes resolutions[package_id as usize]
without ensuring package_id is within resolutions.len(), which can panic; before
using package_id (from self.buffers.resolutions[dep_id as usize]) add a bounds
check (e.g., if package_id as usize >= resolutions.len() { continue } or
otherwise fail-closed) so out-of-range IDs are rejected; update the block around
package_id, invalid_package_id, resolutions, and the eql(resolution, buf, buf)
check to perform this check and avoid panics.

In `@src/install/yarn.rs`:
- Around line 1200-1218: Extract the duplicated remote URL + integrity
validation into a helper function (e.g., validate_remote_url_or_integrity) that
accepts the same inputs used in both blocks: resolved, name_to_use, &entry,
&manager; move the logic that checks starts_with("https://"/"http://"),
manager.scope_for_package_name(...).url.href(),
Integrity::parse(...).tag.is_supported(),
lockfile::bun_lock::url_is_under_registry(...), npm::Registry::DEFAULT_URL and
the codeload.github.com exception into that helper and have it return a
Result<(), bun_core::Error> (or bool) so both original sites call it and
preserve the existing Err(bun_core::err!("InvalidYarnLockfile")) behavior when
validation fails.

In `@src/jsc/bindings/sqlite/JSSQLStatement.cpp`:
- Around line 264-271: The current databaseForHandle(int32_t handle) returns a
VersionSqlite3* while holding databasesLock but releases it before callers
access versionDB->db, allowing close() to null/close the DB and create a
TOCTOU/use-after-close; change the design so handle resolution and DB liveness
are atomic by adding a pin/unpin or reference count inside VersionSqlite3 (e.g.,
add methods like retain() / release() or a mutex inside VersionSqlite3) and
modify databaseForHandle to acquire that pin under databasesLock (or return a
scoped guard object) so callers can safely use versionDB->db without races, and
update close() to wait for or clear that pin before closing the sqlite handle;
apply the same fix pattern where databaseForHandle is used (including the other
referenced block around lines 1799-1821).

In `@src/runtime/api/bun/h2_frame_parser.rs`:
- Around line 656-665: The helper is_forbidden_connection_specific_header
currently whitelists "te: trailers" unconditionally which lets malformed
response header blocks through; change its API to accept an is_request (or
is_response) boolean (e.g., fn is_forbidden_connection_specific_header(name:
&[u8], value: &[u8], is_request: bool) -> bool) and only allow the special-case
"te: trailers" when is_request == true; for responses treat any "te" header as
forbidden. Update every call site that currently invokes
is_forbidden_connection_specific_header (including the spots that validate
inbound requests and inbound responses) to pass the appropriate is_request flag
so responses will be rejected with PROTOCOL_ERROR. Ensure behaviour and tests
reflect the tightened response validation.

In `@src/runtime/cli/pack_command.rs`:
- Around line 3055-3059: The function has_unsafe_tarball_filename_part currently
lets values containing '/' through if they aren't '.' or '..', which allows
inputs like package version "1/2" or unscoped names "foo/bar" to become path
separators later in fmt_tarball_filename; change
has_unsafe_tarball_filename_part to accept a flag allow_scope_separator: bool
and treat any '/' in value as unsafe unless allow_scope_separator is true, keep
the existing checks for "."/".." components and for matches!(c, b'\\' | b':' |
0), and then update the call sites so you pass allow_scope_separator = true when
validating package_name and allow_scope_separator = false when validating
package_version.

In `@src/url/lib.rs`:
- Around line 428-434: The s3_path method is using an off-by-one offset when
slicing href: because parse_protocol() stores protocol without the colon, the
separator is "://", so change the slice offset in s3_path from
self.protocol.len() + 2 to self.protocol.len() + 3 to skip the full "://";
update the return branch in pub fn s3_path(&self) -> &'a [u8] that currently
uses self.protocol.len() + 2 to use +3 so s3://bucket/key yields "/bucket/key"
instead of starting with "//".

In `@test/cli/install/bun-install-lifecycle-scripts.test.ts`:
- Around line 3861-3867: Move the assertion that checks the process exit code
(the `expect(await exited).toBe(0)` call) to occur immediately after the
stdout/stderr assertions and before any filesystem checks; specifically, in the
test blocks where `exists(join(packageDir, ...))` is asserted (including the
other occurrence near the same install phase), place `expect(await
exited).toBe(0)` before the `exists(...)` assertions so exit failure is reported
before downstream file-state assertions.

In `@test/cli/install/bun-install.test.ts`:
- Line 9415: Replace the overly-broad assertion on the error (the
expect(err).toMatch(/Invalid dependency name|is not a valid install folder
name/)) with an exact check for the extraction-layer refusal message: assert
that the error equals or contains the exact string "Refusing to install package
with invalid name" so the test fails if a different layer or message triggers
the rejection.

In `@test/cli/install/migration/yarn-lock-migration.test.ts`:
- Around line 1509-1521: The test currently awaits each process's exit
(rejectedResult.exited and the other spawn's exited) before consuming their
stdout/stderr, which reduces diagnostic output; update both spawned migrate runs
(references: rejectedResult and the other spawn result) to first read stdout and
stderr via new Response(<result>.stdout).text() and new
Response(<result>.stderr).text() and only then await <result>.exited so that
stdout/stderr are captured and included in assertions or error messages.

In `@test/js/bun/http/serve.test.ts`:
- Around line 1134-1149: The raw-connection promise created via
Promise.withResolvers<void>() used with Bun.connect currently wires the socket
handlers data/end/close to resolve but doesn't handle socket errors; add an
error handler on the socket object passed to Bun.connect that calls the promise
reject (from promise.withResolvers) so any socket "error" event rejects the
promise (use the same reject used alongside resolve) to avoid hanging tests;
look for the Promise.withResolvers and the Bun.connect call and add
socket.error(...) => reject(err) (or equivalent) alongside the existing
data/end/close handlers.

In `@test/js/bun/sqlite/sqlite.test.js`:
- Around line 1857-1860: The test currently collects proc.stdout, proc.stderr
and proc.exited into stdout, stderr and exitCode but only asserts stdout and
exitCode; add an assertion for stderr (e.g. expect(stderr.trim()).toBe("") or
another appropriate expectation) so failures show stderr output like the other
tests; update the block around the Promise.all result and the expect calls
referencing stdout, stderr, exitCode to include this new stderr assertion.

---

Outside diff comments:
In `@src/jsc/bindings/bun-spawn.cpp`:
- Around line 41-45: The current fd-sweep clamps maxfd to 65536 in getMaxFd(),
causing closeRangeLoop()/closeRangeOrLoop() to stop at that exclusive bound and
leave fds >= 65536 open; update getMaxFd() (and any place using the 65536 magic)
to query the process soft limit via getrlimit(RLIMIT_NOFILE) and use that as the
sweep upper bound, falling back to a small conservative default only if
getrlimit fails, and preserve the existing logic that treats end == INT_MAX as
“no end” so end+1 isn't applied when calling
closeRangeOrLoop()/closeRangeLoop(); ensure bun_close_range() failure still
triggers the loop fallback that uses the getrlimit-derived bound.

In `@src/parsers/yaml.rs`:
- Around line 2326-2359: The alias expansion budget is only set once in
Parser::init so parse_stream() reusing the Parser across multiple documents can
run out of budget on later documents; update the start-of-document path (where
parse_stream begins processing each new document) to reset
parser.alias_expansion_budget = Parser::MAX_ALIAS_EXPANSION (and optionally
parser.merge_props_budget = MappingProps::MAX_MERGED_PROPERTIES) so each
document gets a fresh per-document budget; locate the document-start code
invoked by parse_stream and add this reset before parsing the document body.

In `@src/runtime/cli/pack_command.rs`:
- Around line 2252-2274: After re-reading package_name/version you must re-run
the restricted-access scoped-name validation so lifecycle scripts can't remove a
required scope; after the package_name is reassigned (the block that sets
package_name from json.root.get("name") and validates emptiness), check if
access == Access::Restricted and if so validate package_name still has a scope
and otherwise return PackError::RestrictedUnscopedPackage (same logic used in
the first-read path). Place this check immediately after the package_name
re-read/validation and mirror the original scoped-name validation routine used
earlier.
🪄 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: 21411465-93d5-40f8-a9c4-7960234a2e6d

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee835b and f515877.

📒 Files selected for processing (78)
  • packages/bun-usockets/src/crypto/openssl.c
  • packages/bun-uws/src/HttpContext.h
  • packages/bun-uws/src/HttpParser.h
  • src/http/HTTPContext.rs
  • src/http/lib.rs
  • src/http_jsc/websocket_client.rs
  • src/install/PackageManager/PackageManagerOptions.rs
  • src/install/PackageManager/security_scanner.rs
  • src/install/TarballStream.rs
  • src/install/extract_tarball.rs
  • src/install/lockfile.rs
  • src/install/yarn.rs
  • src/io/PipeReader.rs
  • src/js/internal/assert/assertion_error.ts
  • src/js/internal/sql/shared.ts
  • src/js/node/_http2_upgrade.ts
  • src/js/node/_http_client.ts
  • src/js/node/net.ts
  • src/jsc/bindings/FormatStackTraceForJS.cpp
  • src/jsc/bindings/SQLClient.cpp
  • src/jsc/bindings/bun-spawn.cpp
  • src/jsc/bindings/sqlite/JSSQLStatement.cpp
  • src/md/line_analysis.rs
  • src/md/links.rs
  • src/md/ref_defs.rs
  • src/parsers/yaml.rs
  • src/runtime/api/bun/h2_frame_parser.rs
  • src/runtime/bake/DevServer.rs
  • src/runtime/bake/dev_server/mod.rs
  • src/runtime/cli/pack_command.rs
  • src/runtime/cli/package_manager_command.rs
  • src/runtime/cli/repl.rs
  • src/runtime/crypto/pwhash.rs
  • src/runtime/node/assert/myers_diff.rs
  • src/runtime/node/node_fs_watcher.rs
  • src/runtime/server/RequestContext.rs
  • src/runtime/socket/Listener.rs
  • src/runtime/socket/socket_body.rs
  • src/runtime/webcore/Request.rs
  • src/runtime/webcore/Response.rs
  • src/runtime/webcore/fetch.rs
  • src/runtime/webcore/fetch/FetchTasklet.rs
  • src/runtime/webcore/s3/list_objects.rs
  • src/sql/mysql/protocol/ColumnDefinition41.rs
  • src/sql_jsc/mysql/MySQLConnection.rs
  • src/standalone_graph/StandaloneModuleGraph.rs
  • src/url/lib.rs
  • src/uws/lib.rs
  • src/uws_sys/Response.rs
  • src/uws_sys/h3.rs
  • test/cli/install/bun-install-lifecycle-scripts.test.ts
  • test/cli/install/bun-install-registry.test.ts
  • test/cli/install/bun-install.test.ts
  • test/cli/install/bun-pack.test.ts
  • test/cli/install/bun-pm.test.ts
  • test/cli/install/migration/yarn-lock-migration.test.ts
  • test/js/bun/http/bun-serve-routes.test.ts
  • test/js/bun/http/serve.test.ts
  • test/js/bun/md/md-edge-cases.test.ts
  • test/js/bun/net/socket.test.ts
  • test/js/bun/repl/repl.test.ts
  • test/js/bun/s3/s3-list-encode-overflow.test.ts
  • test/js/bun/s3/s3-list-objects.test.ts
  • test/js/bun/s3/s3-requester-pays.test.ts
  • test/js/bun/sqlite/sqlite.test.js
  • test/js/bun/util/password.test.ts
  • test/js/bun/yaml/yaml.test.ts
  • test/js/node/assert/assert-typedarray-deepequal.test.ts
  • test/js/node/http/node-http.test.ts
  • test/js/node/http2/node-http2.test.js
  • test/js/node/tls/node-tls-server.test.ts
  • test/js/node/watch/fs.watch.test.ts
  • test/js/sql/sql-mysql.test.ts
  • test/js/sql/sql.test.ts
  • test/js/sql/tls-sql.test.ts
  • test/js/web/fetch/fetch.test.ts
  • test/js/web/fetch/response.test.ts
  • test/js/web/websocket/websocket-client-short-read.test.ts

Comment thread src/install/lockfile.rs
Comment thread src/install/yarn.rs Outdated
Comment thread src/jsc/bindings/sqlite/JSSQLStatement.cpp
Comment thread src/runtime/api/bun/h2_frame_parser.rs
Comment thread src/runtime/cli/pack_command.rs
Comment thread test/cli/install/bun-install-lifecycle-scripts.test.ts Outdated
Comment thread test/cli/install/bun-install.test.ts Outdated
Comment thread test/cli/install/migration/yarn-lock-migration.test.ts Outdated
Comment thread test/js/bun/http/serve.test.ts Outdated
Comment thread test/js/bun/sqlite/sqlite.test.js Outdated
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

The package-binary-size failure on build 58594 is baseline drift, not growth from this PR. The check compares against the latest finished main build at the time it runs: build 58594 compared against main #58579 (commit 3ec6669, PR #31456 "embed browser polyfills zstd-compressed"), which shrank every release binary by ~830 KB (bun-linux-x64: 74,758,936 -> 73,906,968). The sibling builds 58512/58552 passed because they ran before #58579 existed and compared against main #58481 (4ee835b, this branch's merge-base), showing +0.0 KB. Measured against its own merge-base build #58481, this PR's binaries are byte-identical on all linux glibc/android/baseline targets (e.g. bun-linux-x64: 74,758,936 on both) and at most +16 KB on the others (freebsd/musl +16,384 B, windows-x64 +12 KB, darwin-aarch64 +16 B) — far under the 0.5 MB threshold. There is nothing in this PR's compiled code to shrink; merging current main (which contains #31456) into the branch will make the check compare cleanly on the next run.

Comment thread test/vendor.json Outdated
"repository": "https://github.com/elysiajs/elysia",
"tag": "1.4.28"
"tag": "1.4.28",
"skipTests": {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Undo these changes. We should not add breaking changes like this that nobody asked for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reverted. The Response constructor and Response.json() accept bodies on 101/103/204/205/304 again exactly as released 1.4 does (new Response("x", { status: 204 }).text() still resolves to "x", .body is unchanged), and the Elysia skipTests entries are gone from test/vendor.json — the vendored Elysia suites pass with no skips. The only remaining piece is on the write side of Bun.serve: for those statuses no message-body bytes are written to the socket (status line and headers are untouched), which is not observable from JS, plus a raw-socket test for it in serve.test.ts. If you'd rather the wire output stay exactly as before too, I can drop that part as well — leaving this thread open for you.

@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

🤖 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/server/RequestContext.rs`:
- Around line 3563-3566: The short-circuit for status codes 101/103/204/205/304
currently calls do_render_blob(), which eventually invokes render_bytes() and
can write self.blob — causing leaked/stale body bytes; change the no-body path
so it does not call do_render_blob() (or implement a no_body_render variant) and
ensure render_bytes() is not invoked for these status codes (or make
do_render_blob() check response.status_code() and skip writing blob for
101/103/204/205/304), and ensure headers/body reflect a true empty body (e.g.,
no Content-Length/body bytes) to avoid writing stale state.
🪄 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: 63086dc1-96ae-49b8-b56f-856a1a84032f

📥 Commits

Reviewing files that changed from the base of the PR and between 0c596d7 and 5d318f0.

📒 Files selected for processing (2)
  • src/runtime/server/RequestContext.rs
  • test/js/bun/http/serve.test.ts

Comment thread src/runtime/server/RequestContext.rs
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Adjusted per maintainer direction in aecab51: explicit tls/ssl options now map to the require mode only — the connection is aborted if the server declines TLS (postgres 'N' SSLRequest reply / MySQL without CLIENT_SSL) before any credentials are sent. Certificate verification behavior is unchanged from main: it is still only enabled when explicitly requested via ca, rejectUnauthorized, or a verify-* sslmode, and a comment documenting this now sits at the mapping site in src/js/internal/sql/shared.ts. The test option changes that were only needed for verify-by-default were reverted (tls-sql.test.ts getOptions back to tls: true, sql-mysql.test.ts back to tls: Bun.file(ca.pem)); the abort-on-decline regression test in tls-sql.test.ts is kept.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

re: src/install/yarn.rs — yarn.lock migration handling of off-registry tarball URLs

Reverted as requested in 7834157: yarn.lock migration once again accepts off-registry tarball URLs without integrity, exactly as on main. The check_remote_tarball_url helper and both of its (new-in-this-PR) call sites are removed, and a short comment now documents that the lenient handling is intentional (yarn v1 lockfiles legitimately contain entries without integrity and migration must not fail closed). The regression test asserting the rejected behavior was removed from yarn-lock-migration.test.ts; migrate.test.ts was never changed on this branch.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

re: src/js/node/_http_client.ts — absolute URI in the path option

Removed per the directive in 16589e7: the branch's wire behavior does not match Node exactly, so r7-32 is reverted (_http_client.ts back to main, its test removed from node-http.test.ts).

Fixture (raw net server echoing the request line + Host header; client: http.request({ host: "127.0.0.1", port, path: <absolute URI> })), identical script run on all three:

Node v24.3.0:

  • path http://other-host.example/admin?x=1 -> GET http://other-host.example/admin?x=1 HTTP/1.1, Host: 127.0.0.1:<port>
  • path https://other-host.example/admin?x=1 -> GET https://other-host.example/admin?x=1 HTTP/1.1, Host: 127.0.0.1:<port>

Branch debug build (with r7-32):

  • http:// path -> GET http://other-host.example/admin?x=1 HTTP/1.1, Host: 127.0.0.1:<port> (matches Node)
  • https:// path -> GET /admin?x=1 HTTP/1.1, Host: 127.0.0.1:<port> (does NOT match Node — target rewritten to origin-form instead of sending the absolute form verbatim)

USE_SYSTEM_BUN (main behavior), for reference:

  • http:// path -> GET http://other-host.example/admin?x=1 HTTP/1.1, Host: other-host.example
  • https:// path -> CONNECT other-host.example:443 HTTP/1.1, Host: other-host.example:443

Since Node sends the absolute form verbatim for the https:// case while the branch rewrites it to path-only, the change fails the parity bar and is removed rather than kept.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

re: src/runtime/api/bun/h2_frame_parser.rs — server-side header validation

Ran the requested comparison against real Node (v24.3.0, http2.createServer, raw frame client: preface + SETTINGS + HEADERS(+DATA), same HPACK encoding as the in-tree test):

case sent to Node server delivered to 'stream' handler wire result
connection: keep-alive header no RST_STREAM stream 1, code 1 (PROTOCOL_ERROR)
transfer-encoding: chunked header no RST_STREAM code 1
duplicated :path pseudo-header no RST_STREAM code 1
pseudo-header after a regular field no RST_STREAM code 1
te: gzip no RST_STREAM code 1
te: trailers yes 200 response
duplicated content-length field no RST_STREAM code 1
content-length: 5 + 3-byte DATA (END_STREAM) yes (delivered first) then RST_STREAM code 1 at end-of-stream (ERR_HTTP2_STREAM_ERROR)
clean request yes 200 response

Branch Bun for the cases this PR changed: each new condition (connection-specific fields incl. te != trailers, repeated pseudo/content-length fields, pseudo-header after a regular field) feeds the pre-existing malformed path, which sends RST_STREAM with PROTOCOL_ERROR on that stream only (no GOAWAY) and never dispatches the headers to JS — the same observable behavior as Node above. The in-tree test "http2 server rejects requests carrying connection-specific or repeated pseudo-headers" drives the identical raw-frame client against Bun's server and asserts exactly that (0 delivered, RST code NGHTTP2_PROTOCOL_ERROR, no HEADERS response); I added a connection: keep-alive case in 28e34ef so (a) is covered verbatim alongside transfer-encoding and the duplicated :path.

On (c): this PR does not add any DATA-vs-content-length mismatch enforcement. The only content-length change is rejecting a repeated content-length header field, which Node also rejects (RST code 1, not delivered). Bun's behavior for a content-length value that disagrees with the DATA length is unchanged by this branch (request still delivered, no reset), so nothing was removed or adjusted there; matching Node's end-of-stream length check would be a separate change.

Keeping the validation. node-http2.test.js passes in full with the added case.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

re: src/js/node/net.ts — server-side socket.authorized default

Verified against real Node.js as requested — the r7-33 change matches Node and is kept (no code change in this round).

Fixture (Node v24.3.0, branch debug build, system Bun 1.4.0; agent1/ca1 certs from test/js/node/tls/fixtures), server-side socket.authorized:

Case Node v24.3.0 This branch Bun 1.4.0 (system)
(a) tls.Server, no requestCert false false true
(b) requestCert:true + valid client cert true true true
(c) requestCert:true, rejectUnauthorized:false, no client cert false (authorizationError=UNABLE_TO_GET_ISSUER_CERT) false (UNABLE_TO_GET_ISSUER_CERT) false (UNABLE_TO_GET_ISSUER_CERT)
(d) http2.createSecureServer, no requestCert (session.socket.authorized) false false true

The branch matches Node on all four cases, including the http2-over-TLS upgrade path; current Bun does not on (a) and (d). This mirrors Node's onServerSocketSecure in lib/_tls_wrap.js, where authorized is only set to true inside the if (this._requestCert) branch when verifyError() is null, and otherwise stays at its constructor default of false. Keeping the net.ts and _http2_upgrade.ts changes and the test in node-tls-server.test.ts as-is.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

@Jarred-Sumner Jarred-Sumner force-pushed the claude/security-round-7 branch from 28e34ef to 372f68c Compare May 28, 2026 20:26
Comment on lines +474 to 486
let mut dir = PathBuffer::uninit();
let fd = get_cache_directory(pm);
let outpath = match bun_sys::get_fd_path(fd, &mut dir) {
Ok(p) => &p[..],
Err(err) => {
Output::pretty_errorln(format_args!(
"{} getting cache directory",
bun_core::Error::from(err).name(),
));
Global::crash();
}
};
let _ = Output::writer().write_all(outpath);

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.

🟡 nit (description text only): the parenthetical "(bun pm cache prints the same path it would delete)" no longer matches the design settled in the resolved thread on this file — bun pm cache intentionally keeps the project-config-honoring get_cache_directory(pm) path while bun pm cache rm uses the process-env-only resolution, so the two can differ when a project-local BUN_INSTALL_CACHE_DIR/.npmrc cache= is set. Consider dropping or rewording the parenthetical (e.g. "bun pm cache continues to print the project-effective cache path").

Extended reasoning...

What this is

This is a documentation-accuracy nit on the PR description text only — the code is correct and intentional. The PR description's pm: bullet reads:

pm: bun pm cache rm resolves the cache directory from the process environment, ignoring project-local overrides for the destructive delete (bun pm cache prints the same path it would delete)

The bolded parenthetical is no longer accurate after the review thread on this file was resolved. Per the implementation at src/runtime/cli/package_manager_command.rs:474-486, bun pm cache (display) calls get_cache_directory(pm), which honors project .env / bunfig / .npmrc cache= overrides; while bun pm cache rm (lines 376-409) builds a fresh process-env-only loader and resolves via fetch_cache_directory_path(&mut process_env, None). The author's own resolution of review thread 3315091045 states this divergence is by design:

The print at line ~489 intentionally keeps the project-config-honoring resolution: bun pm cache reports the cache directory this project's installs actually use … The non-destructive print and the destructive delete answer different questions by design here.

So when a project-local cache override exists, bun pm cache and bun pm cache rm resolve different paths — the parenthetical asserts the opposite.

Step-by-step example

  1. A project's checked-in .npmrc contains cache=/tmp/project-cache; the process environment has only BUN_INSTALL=/home/user/.bun.
  2. bun pm cache reaches line 475, calls get_cache_directory(pm) (which uses pm.env with the project's .npmrc merged), and prints /tmp/project-cache. This is the behavior test/cli/install/npmrc.test.ts ("should convert to utf8 if BOM") locks in.
  3. bun pm cache rm reaches line 378, builds a fresh bun_dotenv::Loader, calls only load_process(), and fetch_cache_directory_path returns /home/user/.bun/install/cache, which is what gets deleted.
  4. The path printed in step 2 is not the path deleted in step 3.

Addressing the "not actionable / author already knows" objection

One verifier argued this is redundant meta-commentary because the author wrote both the description and the resolution, so he's aware of the behavior. That's true — but knowing the code behavior and noticing that one parenthetical in a long, actively-maintained description has gone stale are different things. This PR description is unusually carefully curated: it already has a "Changes tried and reverted during review (rationale)" section that was added as the branch evolved, and the pm: bullet itself was clearly written for an earlier iteration before thread 3315091045 settled on "they answer different questions by design." It's the same class of drift the author has been correcting elsewhere in the description.

This is also not a duplicate of the resolved thread (3314890827 / 3315091045): that thread was about the code divergence and was resolved as by-design; this is about the PR description text not having been updated to reflect that resolution.

Why nothing else catches it

PR description text isn't covered by tests or CI. The new regression tests in test/cli/install/bun-pm.test.ts correctly assert the rm behavior, and npmrc.test.ts covers the display behavior — both pass; only the prose disagrees.

Impact and fix

Nit only — PR description prose doesn't ship. Worth a one-line edit because (a) the description is otherwise being maintained as an accurate change record, and (b) a reader skimming the description for "what changed in bun pm cache" would take away the wrong invariant. Suggested rewording:

pm: bun pm cache rm resolves the cache directory from the process environment, ignoring project-local overrides for the destructive delete (bun pm cache continues to print the project-effective cache path)

or simply drop the parenthetical.

@Jarred-Sumner Jarred-Sumner merged commit 002dd41 into main May 28, 2026
74 of 78 checks passed
@Jarred-Sumner Jarred-Sumner deleted the claude/security-round-7 branch May 28, 2026 21:38
Jarred-Sumner added a commit that referenced this pull request May 29, 2026
…(round 8) (#31559)

Tightens input validation, bounds checking, and pointer/lifetime
handling across the package manager, node:http/http2, the debug adapter,
the V8 shim, S3, and path/fs internals. Continuation of #31417 and
#31495 (rounds 6–7); same structure — small per-area commits with
regression tests included.

### Package manager / build
- install: validate dependency-alias-derived names at the remaining
cache-index call site; validate git/github resolved values when loading
`bun.lockb` (same check the text lockfile applies)
- compile: bounds-check Mach-O segment offsets/sizes with
overflow-checked arithmetic before patching; a corrupted template now
errors instead of crashing
- webview: the default Chrome profile directory is created with an
unpredictable name and owner-only permissions

### node:http / node:http2 / debugger / V8 shim
- node:http: a `host` option containing URL-reserved delimiters is
treated as an unresolvable hostname instead of being re-parsed into a
different target
- node:http2: the raw-socket TLS upgrade path forwards the server's
`requestCert`/`rejectUnauthorized` configuration into the native TLS
config explicitly
- debug adapter: only allowlisted inspector event domains from the debug
target are dispatched; the diagnostics signal listener binds to loopback
- v8 shim: `String::Utf8Length`/`WriteUtf8` saturate at `INT_MAX`
instead of wrapping for very large strings

### Runtime / fs / S3 / path
- node:fs: the read path re-fetches the destination buffer after
argument coercion can run user code; buffer-backed path arguments are
pinned for the duration of async operations
- S3: a malformed multipart upload ID from the server surfaces as a
request error instead of aborting the process
- node:path: `path.format` sizes its scratch buffer for the implicitly
inserted extension separator, removing a reachable abort

### Not changed
The bin-link containment behavior for dotless symlinked targets and the
binary-lockfile trust policy are unchanged (both previously decided);
the file-backed Blob deserialization finding is already covered by the
open #31329.

### Tests
9 validated regression tests (each fails on the released build — two of
them by crashing it — and passes here), plus 2 regression-locked tests
for repo-TypeScript debugger changes. The Chrome profile-dir and
h2-upgrade changes ship without standalone tests (no non-flaky
observable in existing files; noted per commit).

`cargo check` and the cross-target checks pass on all 10 CI targets; the
touched suites pass locally with no new failures versus main.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Jarred-Sumner added a commit that referenced this pull request Jun 2, 2026
…ms (round 9) (#31606)

Tightens input validation, pointer/lifetime handling, and
Node-compatibility across the package manager, bun-lambda,
node:zlib/net/http, Glob, the transpiler cache, postgres, and
HTMLRewriter. Continuation of #31417, #31495, and #31559; same structure
— small per-area commits with regression tests included.

### Scaffolding / Lambda
- create: dependency names detected from scanned source files are passed
to the spawned installer as positionals (after `--`), so a specifier can
never be interpreted as a flag
- bun-lambda: event-to-Request conversion builds the URL from the
request context's domain; client-supplied path bytes can no longer
change the URL authority

### node runtime
- node:zlib: native compression handles re-resolve the caller-supplied
output array on every write completion instead of caching a raw pointer,
and reject detached/resized buffers
- node:net: BlockList subnet matching handles IPv4-mapped IPv6 rules the
same way Node does (verified against Node's behavior)
- node:http: duplicate request headers follow Node's IncomingMessage
precedence — first value wins for singleton headers, others are joined,
`set-cookie` is arrayed (verified against Node's behavior)
- Glob: an oversized `cwd` option throws a catchable error instead of
aborting the process

### Runtime internals
- transpiler: cached module metadata records with out-of-range string
indices are rejected at load (previously a corrupted cache record could
crash the process)
- postgres: unknown backend messages are skipped by exactly their
declared length, and NotificationResponse is consumed through the
protocol decoder, so the stream can no longer desynchronize
- HTMLRewriter: `comment.replace()` removes the original comment and
emits only the replacement (previously both appeared in the output)

### Not changed
The bin-link containment behavior for symlinked targets is unchanged
(previously decided); the recursive-delete traversal change is already
covered by the open #31337.

### Tests
11 regression tests across the touched areas. Most are validated to fail
on the released build and pass here — including one where the released
build segfaults on a corrupted cache record and one where it hangs on an
oversized Glob cwd. The zlib write-state tests are regression-only (the
released build does not exhibit the failure deterministically without
ASAN).

`cargo check` and the cross-target checks pass on all 10 CI targets; the
touched suites pass locally with no new failures versus main.

---------

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