Skip to content

Hardening: input validation and bounds checking across 12 subsystems (round 8)#31559

Merged
Jarred-Sumner merged 29 commits into
mainfrom
claude/security-round-8
May 29, 2026
Merged

Hardening: input validation and bounds checking across 12 subsystems (round 8)#31559
Jarred-Sumner merged 29 commits into
mainfrom
claude/security-round-8

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

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.

@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator
Updated 11:35 AM PT - May 29th, 2026

@Jarred-Sumner, your commit bd18e22cb70c5dd35429add0d5d273e0780c982d passed in Build #59008! 🎉


🧪   To try this PR locally:

bunx bun-pr 31559

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

bun-31559 --bun

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This PR hardens Bun's security and correctness across debug adapters, executable handling, package management, filesystem APIs, and input validation. It adds event filtering, bounds checking, path traversal prevention, buffer lifecycle management, host/upload-id validation, numeric saturation, and loopback/temp-directory isolation, with 12 new test suites covering the changes.

Changes

Security and Safety Improvements

Layer / File(s) Summary
Debug adapter event filtering and loopback binding
packages/bun-debug-adapter-protocol/src/debugger/adapter.ts, signal.ts, sourcemap.test.ts
Introduce inspectorEventDomains allowlist and isInspectorEvent() helper to forward only recognized inspector protocol events to adapter listeners. Bind TCPSocketSignal TCP server to loopback 127.0.0.1 explicitly. Tests verify genuine Heap.garbageCollected events reach listeners while DAP-named events are blocked, and verify non-loopback connection attempts fail.
Mach-O compilation bounds validation
src/exe_format/macho.rs, test/bundler/bundler_compile.test.ts
Use checked arithmetic for __BUN/__bun section offset calculations; bounds-check both original_bun_end and original_data_end as usize before memmove; reject with OffsetOutOfRange if bounds exceed buffer. Integration test confirms out-of-bounds templates fail with exit code 1 and in-bounds templates succeed.
Package management path traversal prevention
src/install/extract_tarball.rs, src/install/lockfile/bun.lockb.rs, test/cli/install/bun-install.test.ts, bun-lockb.test.ts
Tighten move_to_cache_directory to require both basename path-safety and is_safe_install_folder_name() check before creating index entries. Add Git/Github resolution validation in lockfile load: reject non-empty resolved tags failing is_safe_resolved_tag with InvalidLockfile. CLI tests verify .. alias is rejected, cache parent remains clean, and corrupted lockfiles are detected and ignored.
Buffer pinning and unpin on error/drop for async filesystem paths
src/jsc/node_path.rs, src/runtime/node/types.rs, test/js/node/fs/fs.test.ts
Update PathLike::Drop to explicitly unpin pinned Buffer variant. Modify types.rs::from_js_with_allocator to conditionally pin buffers for async paths, validate them, and unpin on validation failure. Regression tests ensure readSync detachment attempts are prevented, and fs.promises.writeFile path buffers remain attached during options reading.
Input validation, bounds checking, and numeric saturation
src/js/node/_http_client.ts, src/jsc/bindings/v8/V8String.cpp, src/runtime/node/node_fs.rs, src/runtime/node/path.rs, src/runtime/webcore/s3/multipart.rs, test/js/node/http/node-http.test.ts, test/js/node/path/parse-format.test.js, test/js/bun/s3/s3.test.ts, test/v8/v8.test.ts
Add INVALID_HOST_CHAR_REGEX to reject host strings containing URL delimiters with ENOTFOUND on next tick. Clamp String::Utf8Length output to INT32_MAX saturation. Tighten S3 upload-id validation to reject non-ASCII bytes. Adjust path formatting buffer sizing. Tests verify host delimiter confusion fails safely, UTF-8 saturation behavior, S3 non-ASCII rejection, extreme path formatting, and DNS error propagation.
Process and network isolation changes
src/js/node/_http2_upgrade.ts, src/runtime/webview/ChromeProcess.rs
Pass requestCert and rejectUnauthorized from Http2SecureServer into upgradeDuplexToTLS for consistent mutual-TLS behavior. Change Chrome spawn --user-data-dir fallback from fixed PID-based path to unique temp directory with 0o700 permissions.

Possibly related PRs

  • oven-sh/bun#31339: Hardens src/install/extract_tarball.rs by adding/strengthening is_safe_install_folder_name(name) checks in move_to_cache_directory for NPM cache/index handling.

Suggested reviewers

  • alii
  • cirospaciari
  • pfgithub
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Hardening: input validation and bounds checking across 12 subsystems (round 8)' clearly and specifically summarizes the main objective of the pull request—hardening via input validation and bounds checking across multiple subsystems.
Description check ✅ Passed The PR description comprehensively addresses both template sections: it explains what the PR does across 12 subsystems with detailed sub-sections, and verifies correctness through 9 validated regression tests plus CI passing on 10 targets with no new failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't find any bugs, but this hardening round spans 12 security-sensitive subsystems (unsafe Mach-O pointer arithmetic, TLS auth forwarding, path-traversal guards, buffer lifetime/pinning in fs) and CI is currently red on most build-rust targets, so it's worth a human look before merging.

Extended reasoning...

Overview

This PR applies input-validation and bounds-checking hardening across 23 files in 12 distinct subsystems: the package manager (cache-index alias validation, binary lockfile git-tag validation), bun build --compile Mach-O patching, node:http host-option validation, node:http2 raw-socket TLS upgrade, the debug adapter (inspector event allowlisting, loopback-only signal listener), the V8 shim (Utf8Length/WriteUtf8 saturation), node:fs (buffer re-fetch after coercion, async path pinning), S3 multipart upload-ID validation, node:path scratch-buffer sizing, and the Chrome webview profile-dir creation. Each change ships with a regression test.

Security risks

Nearly every change here is security-relevant by design: path-traversal mitigation in extract_tarball.rs/lockfile.rs, host/SSRF confusion mitigation in _http_client.ts, mTLS config propagation in _http2_upgrade.ts, OOB memmove prevention in macho.rs (unsafe Rust with raw pointer arithmetic), use-after-detach prevention in node_fs.rs/types.rs/node_path.rs, and reducing local-network attack surface in signal.ts. The changes are defensive (tightening, not loosening), but several touch unsafe blocks and GC-lifetime invariants where a subtle mistake would itself be a memory-safety bug. The WriteUtf8 change from SIZE_MAX to INT_MAX for negative length is a behavior change worth a second look.

Level of scrutiny

High. This is exactly the category of PR — security hardening across many production-critical paths, with unsafe Rust and TLS/auth handling — where the approval criteria say not to auto-approve. The individual patches are small and well-commented, and the regression tests are thorough, but the breadth and the memory-safety/auth surface argue for human review.

Other factors

robobun reports build-rust failures on 9+ CI targets for the merge commit (the autofix commit landed afterward; the description claims all 10 targets pass, so there's a discrepancy worth confirming). No prior human or claude[bot] review exists on the timeline. The bug-hunting system found nothing, which is reassuring, but does not change the scrutiny bar for a 12-subsystem security PR.

Comment thread src/install/lockfile.rs Outdated
Comment thread src/runtime/node/types.rs Outdated
Valid::path_null_bytes(buffer.slice(), ctx)?;

let buffer = if arguments.will_be_async {
Buffer::from_js_pinned(ctx, arg).unwrap_or(buffer)

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 makes it slower. One pass. Not 2.

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.

Restructured to one pass. When the call will be async, the single conversion is now Buffer::from_js_pinned (pin + one extraction), with the plain conversion as the fallback — same shape as the existing async StringOrBuffer path earlier in this file — and the sync path is a single from_typed_array/from_array_buffer exactly as on main. The second walk over the argument is gone. Validation runs on the pinned buffer and the throw paths release the pin before propagating, so the Drop balance in PathLike stays correct. String path and sync path do the same work as main; the async buffer path only adds the pin itself. The fs.promises.writeFile buffer-path test on the branch still passes and still fails on released Bun.

Comment thread src/runtime/node/node_fs.rs Outdated
} else {
0.0
};
let buffer = if length_value.is_some_and(|arg| !arg.is_number()) {

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.

Do not make it slower. One pass. Not two.

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.

Reordered to a single pass — the re-fetch is gone. The buffer pointer/length is now captured exactly once, immediately after the offset/length arguments are processed (length's ToNumber is the only step there that can re-enter JS), so nothing captured before that point is used after it. The all-numbers fast path does exactly the same operations as before this change set: fd, offset validation, length ToNumber, then one Buffer::from_js — no added conversions or branches, just a different order. The readSync detach test still fails on the released build and passes here, test-fs-read* parallel tests pass, and a readSync loop microbench is at parity with main.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

The Windows failures on the Utf8Length bounds test were not an allocation/string-limit problem: in all three Windows job logs the addon built and the oversized string correctly reported "Utf8Length = 2147483647". The only mismatch was a trailing \r on the first output line (printf emits \r\n on Windows and the test split stdout on "\n"). Fixed in 4e2b03a by splitting on /\r?\n/ instead, which keeps the Windows coverage intact rather than skipping it; linux/macos validation and the C++ change are unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@packages/bun-debug-adapter-protocol/src/debugger/sourcemap.test.ts`:
- Around line 72-74: Replace the pre-allocated port flow that uses
getAvailablePort() with binding the socket to port 0: construct TCPSocketSignal
with port: 0, await signal.ready, then read the assigned port from the signal
instance (use the property/method on TCPSocketSignal that exposes the actual
listening port) instead of using the previously created port variable; remove
getAvailablePort() usage and any reliance on a precomputed port when invoking
TCPSocketSignal and subsequent test code.

In `@test/cli/install/bun-lockb.test.ts`:
- Around line 346-367: The test reads the process exit status into the variable
code (via exited) but never asserts it; add a final assertion for the expected
non-zero exit code at the end of the tampered-lockfile test block (after the
exists() assertion). Specifically, after the existing expectations referencing
spawn, stderrForInstall, out, and exists(join(packageDir, "node_modules",
"dep")), add an assertion on code (e.g., expect(code).not.toBe(0) or the
concrete non-zero exit value your suite uses) so the exit status is checked last
and failures remain diagnosable.
- Around line 269-270: Update the test's hardcoded git URL to use port 0 instead
of 1: change the string assigned to gitUrl (currently
"git+ssh://git@127.0.0.1:1/example/repo.git") to use ":0" so it becomes
"git+ssh://git@127.0.0.1:0/example/repo.git" (leave sha untouched); this ensures
the test follows the harness rule to always use port: 0 and avoids
environment-dependent failures when running the tests.
🪄 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: 8f90be47-add7-4797-bcd6-8c0b83add65b

📥 Commits

Reviewing files that changed from the base of the PR and between f3ccf76 and 4e2b03a.

📒 Files selected for processing (23)
  • packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
  • packages/bun-debug-adapter-protocol/src/debugger/signal.ts
  • packages/bun-debug-adapter-protocol/src/debugger/sourcemap.test.ts
  • src/exe_format/macho.rs
  • src/install/extract_tarball.rs
  • src/install/lockfile/bun.lockb.rs
  • src/js/node/_http2_upgrade.ts
  • src/js/node/_http_client.ts
  • src/jsc/bindings/v8/V8String.cpp
  • src/jsc/node_path.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/node/path.rs
  • src/runtime/node/types.rs
  • src/runtime/webcore/s3/multipart.rs
  • src/runtime/webview/ChromeProcess.rs
  • test/bundler/bundler_compile.test.ts
  • test/cli/install/bun-install.test.ts
  • test/cli/install/bun-lockb.test.ts
  • test/js/bun/s3/s3.test.ts
  • test/js/node/fs/fs.test.ts
  • test/js/node/http/node-http.test.ts
  • test/js/node/path/parse-format.test.js
  • test/v8/v8.test.ts

Comment thread packages/bun-debug-adapter-protocol/src/debugger/sourcemap.test.ts Outdated
Comment thread test/cli/install/bun-lockb.test.ts Outdated
Comment thread test/cli/install/bun-lockb.test.ts
Comment thread src/js/node/_http_client.ts
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Looked into the darwin-14-x64 timeout for "rejects a binary lockfile whose git resolved tag contains path separators" — classifying it as runner congestion, not a hang in the test:

  • The setup phase never contacts the git remote: it migrates the checked-in package-lock.json (the resolved commit is transcribed by the migration code) and --lockfile-only saves bun.lockb without installing, so no git process is spawned there.
  • The only git invocation in the whole test is the fallback resolve after "Ignoring lockfile". Its remote is ssh://git@127.0.0.1:0/... — a literal loopback address (no DNS) on port 0, and OpenSSH rejects -p 0 with Bad port '0' before any connection or auth attempt. Verified locally: git clone --bare / git ls-remote against that exact URL exit in milliseconds. Bun also sets GIT_ASKPASS=echo and GIT_SSH_COMMAND="ssh -oStrictHostKeyChecking=accept-new" for these spawns, so no credential or host-key prompt is reachable in any case.
  • The same job timed out main's untouched lockfile-version-2.test.ts "unsafe git .bun-tag is rejected only at version 2", which uses the identical unreachable-loopback-ssh pattern (port 1) and is stable on main — pointing at the runner, not the test content.

So nothing in the test can block indefinitely on macOS; the 90s was spent on a congested runner running three sequential debug-build installs plus the shared Verdaccio fixture. No code change.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

Investigated with the build 58946 darwin-14-x64 job logs rather than guessing: every attempt of the new test spends its full 90s with the last output being an interactive ssh prompt (git@127.0.0.1) Password: followed by "killed 2 dangling processes", and main's lockfile-version-2 "unsafe git .bun-tag is rejected only at version 2" shows the exact same signature. So this is not the runner stalling — both tests really do hang.

Root cause: the "unreachable" loopback port in the ssh URL (:0 here, :1 in lockfile-version-2) is folded into the path by Bun's scp-style URL correction (try_ssh -> hosted_git_info::correct_url replaces the last colon with /), so git/ssh ends up dialing 127.0.0.1:22. The macOS CI runners have a local sshd, publickey auth fails, and ssh falls back to an interactive password prompt on /dev/tty (Bun's default GIT_SSH_COMMAND has no BatchMode and GIT_ASKPASS only covers git's own prompts), hanging until the 90s test timeout. Linux/Windows runners have nothing listening on 127.0.0.1:22, so the same fallback git clone fails in milliseconds — hence the few-second runtimes there.

Fix in 7a35ffa (no assertion weakened, test not skipped): the spawned installs now pass GIT_SSH_COMMAND="ssh -oBatchMode=yes -oStrictHostKeyChecking=accept-new -oConnectTimeout=5" and GIT_TERMINAL_PROMPT=0, so the fallback git clone fails fast everywhere instead of prompting. The same one-line env override is applied to the v1 block of lockfile-version-2.test.ts since it hits the identical hang on these jobs and a rebuild would not clear it. No rebuild triggered — the failure is in the test fixtures, not the infrastructure.

@Jarred-Sumner Jarred-Sumner merged commit bb94ed4 into main May 29, 2026
7 of 40 checks passed
@Jarred-Sumner Jarred-Sumner deleted the claude/security-round-8 branch May 29, 2026 18:00
@robobun robobun mentioned this pull request May 29, 2026
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