When `getPeerCertificate(true)` returns undefined (e.g. due to SSL
handle or cert chain being unavailable), `checkServerIdentity` would
be called with `undefined` as the cert argument, causing a TypeError:
"Cannot destructure property 'subject' from null or undefined value".
Add a null check on the cert before passing it to `checkServerIdentity`
in both TLS handshake handlers in net.ts.
Closes#23556
Co-Authored-By: Claude <noreply@anthropic.com>
## Summary
- Fix infinite loop in `Bun.stripANSI()` when input contains control
characters in the `0x10-0x1F` range that are not ANSI escape introducers
(e.g. `0x16` SYN, `0x19` EM)
- The SIMD fast-path in `findEscapeCharacter` matched the broad range
`0x10-0x1F` / `0x90-0x9F`, but `consumeANSI` only handles a subset of
those characters. When `consumeANSI` returned the same pointer for an
unrecognized byte, the main loop in `stripANSI` never advanced, causing
a hang in release builds and an assertion failure in debug builds.
- Fix verifies SIMD candidates through `isEscapeCharacter()` before
returning, matching the behavior the scalar fallback path already had
## Test plan
- [x] Added regression test in `test/regression/issue/27014.test.ts`
with 4 test cases
- [x] Verified test hangs with system bun (v1.3.9) confirming the bug
- [x] All 4 new tests pass with debug build
- [x] All 265 existing `stripANSI.test.ts` tests pass with debug build
Closes#27014🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Adds self-contained HTML output mode: `--compile --target=browser`
(CLI) or `compile: true, target: "browser"` (`Bun.build()` API)
- Produces HTML files with all JS, CSS, and assets inlined directly:
`<script src="...">` → inline `<script>`, `<link rel="stylesheet">` →
inline `<style>`, asset references → `data:` URIs
- All entrypoints must be `.html` files when using `--compile
--target=browser`
- Validates: errors if any entrypoints aren't HTML, or if `--splitting`
is used
- Useful for distributing `.html` files that work via `file://` URLs
without needing a web server or worrying about CORS restrictions
## Test plan
- [x] Added `test/bundler/standalone.test.ts` covering:
- Basic JS inlining into HTML
- CSS inlining into HTML
- Combined JS + CSS inlining
- Asset inlining as data URIs
- CSS `url()` references inlined as data URIs
- Validation: non-HTML entrypoints rejected
- Validation: mixed HTML/non-HTML entrypoints rejected
- Validation: splitting rejected
- `Bun.build()` API with `compile: true, target: "browser"`
- CLI `--compile --target=browser`
- Minification works with compile+browser
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
### What does this PR do?
When Error.captureStackTrace(e, fn) is called with a function that isn't
in the call stack, all frames are filtered out and e.stack should return
just the error name and message (e.g. "Error: test"), matching Node.js
behavior. Previously Bun returned undefined because:
1. The empty frame vector replaced the original stack frames via
setStackFrames(), but the lazy stack accessor was only installed when
hasMaterializedErrorInfo() was true (i.e. stack was previously
accessed). When it wasn't, JSC's internal materialization saw the
empty/null frames and produced no stack property at all.
2. The custom lazy getter returned undefined when stackTrace was
nullptr, instead of computing the error name+message string with zero
frames.
Fix: always force materialization before replacing frames, always
install the custom lazy accessor, and handle nullptr stackTrace in the
getter by computing the error string with an empty frame list.
### How did you verify your code works?
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## Summary
Speculative fix for
[BUN-1K54](https://bun-p9.sentry.io/issues/7260165386/) — a segfault in
`JSC__JSValue__fromEntries` with 238 occurrences on Windows x86_64 (Bun
1.3.9).
## Problem
`JSC__JSValue__fromEntries` wraps its property insertion loop inside a
`JSC::ObjectInitializationScope`. This scope is designed for fast,
allocation-free object initialization using `putDirectOffset`. However,
the code uses `putDirect` (which can trigger structure transitions)
along with `toJSStringGC` and `toIdentifier` (which allocate on the GC
heap).
In **debug builds**, `ObjectInitializationScope` includes `AssertNoGC`
and `DisallowVMEntry` guards that would catch this misuse immediately.
In **release builds**, the scope is essentially a no-op (only emits a
`mutatorFence` on destruction), so GC can silently trigger during the
loop. When this happens, the GC may encounter partially-initialized
object slots containing garbage values, leading to a segfault.
## Fix
- Remove the `ObjectInitializationScope` block, since `putDirect` with
allocating helpers is incompatible with its contract.
- Add `RETURN_IF_EXCEPTION` checks inside each loop iteration to
properly propagate exceptions (e.g., OOM during string allocation).
## Test
Added a regression test that creates a `FileSystemRouter` with 128
routes and accesses `router.routes` under GC pressure. Verified via
temporary `fprintf` logging that the test exercises the modified
`JSC__JSValue__fromEntries` code path with `initialCapacity=128,
clone=true`.
Note: The original crash is a GC timing issue that cannot be
deterministically reproduced in a test. This test validates correctness
of the code path rather than reproducing the specific crash.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Fixes#12117, #24118, #25948
When a TCP socket is upgraded to TLS via `tls.connect({ socket })`,
`upgradeTLS()` creates **two** `TLSSocket` structs — a TLS wrapper and a
raw TCP wrapper. Both are `markActive()`'d and `ref()`'d. On close, uws
fires `onClose` through the **TLS context only**, so the TLS wrapper is
properly cleaned up, but the raw TCP wrapper's `onClose` never fires.
Its `has_pending_activity` stays `true` forever and its `ref_count` is
never decremented, **leaking one raw `TLSSocket` per upgrade cycle**.
This affects any code using the `tls.connect({ socket })` "starttls"
pattern:
- **MongoDB Node.js driver** — SDAM heartbeat connections cycle TLS
upgrades every ~10s, causing unbounded memory growth in production
- **mysql2** TLS upgrade path
- Any custom starttls implementation
### The fix
Adds a `defer` block in `NewWrappedHandler(true).onClose` that cleans up
the raw TCP socket when the TLS socket closes:
```zig
defer {
if (!this.tcp.socket.isDetached()) {
this.tcp.socket.detach();
this.tcp.has_pending_activity.store(false, .release);
this.tcp.deref();
}
}
```
- **`isDetached()` guard** — skips cleanup if the raw socket was already
closed through another code path (e.g., JS-side `handle.close()`)
- **`socket.detach()`** — marks `InternalSocket` as `.detached` so
`isClosed()` returns `true` safely (the underlying `us_socket_t` is
freed when uws closes the TLS context)
- **`has_pending_activity.store(false)`** — allows JSC GC to collect the
raw socket's JS wrapper
- **`deref()`** — balances the `ref()` from `upgradeTLS`; the remaining
ref is the implicit one from JSC (`ref_count.init() == 1`). When GC
later calls `finalize()` → `deref()`, ref_count hits 0 and `deinit()`
runs the full cleanup chain (markInactive, handlers, poll_ref,
socket_context)
`markInactive()` is intentionally **not** called in the defer — it must
run inside `deinit()` to avoid double-freeing the handlers struct.
### Why Node.js doesn't have this bug
Node.js implements TLS upgrades purely in JavaScript/C++ with OpenSSL,
where the TLS wrapper takes ownership of the underlying socket. There is
no separate "raw socket wrapper" that needs independent cleanup.
## Test Results
### Regression test
```
$ bun test test/js/node/tls/node-tls-upgrade-leak.test.ts
1 pass, 0 fail
```
Creates 20 TCP→TLS upgrade cycles, closes all connections, runs GC,
asserts `TLSSocket` count stays below 10.
### Existing TLS test suite (all passing)
```
node-tls-upgrade.test.ts 1 pass
node-tls-connect.test.ts 24 pass, 1 skip
node-tls-server.test.ts 21 pass
node-tls-cert.test.ts 25 pass, 3 todo
renegotiation.test.ts 6 pass
```
### MongoDB TLS scenario (patched Bun, 4 minutes)
```
Baseline: RSS=282.4 MB | Heap Used=26.4 MB
Check #4: RSS=166.7 MB | Heap Used=24.2 MB — No TLSSocket growth. RSS DECREASED.
```
## Test plan
- [x] New regression test passes (`node-tls-upgrade-leak.test.ts`)
- [x] All existing TLS tests pass (upgrade, connect, server, cert,
renegotiation)
- [x] MongoDB TLS scenario shows zero `TLSSocket` accumulation
- [x] Node.js control confirms leak is Bun-specific
- [ ] CI passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
### What does this PR do?
### How did you verify your code works?
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fix `<link rel="manifest" href="./manifest.json" />` (and similar
non-JS/CSS URL assets) resulting in 404s when using `Bun.build` with
HTML entrypoints
- The HTML scanner correctly identifies these as `ImportKind.url`
imports, but the bundler was assigning the extension-based loader (e.g.
`.json`) which parses the file instead of copying it as a static asset
- Force the `.file` loader for `ImportKind.url` imports when the
resolved loader wouldn't `shouldCopyForBundling()` and isn't JS/CSS/HTML
(which have their own handling)
## Test plan
- [x] Added `html/manifest-json` test: verifies manifest.json is copied
as hashed asset and HTML href is rewritten
- [x] Added `html/xml-asset` test: verifies `.webmanifest` files are
also handled correctly
- [x] All 20 HTML bundler tests pass (`bun bd test
test/bundler/bundler_html.test.ts`)
- [x] New tests fail on system bun (`USE_SYSTEM_BUN=1`) confirming they
validate the fix
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fix `OutgoingMessage.setHeaders()` incorrectly throwing
`ERR_HTTP_HEADERS_SENT` on brand new `ClientRequest` instances
- The guard condition `this[headerStateSymbol] !==
NodeHTTPHeaderState.none` failed when `headerStateSymbol` was
`undefined` (since `ClientRequest` doesn't call the `OutgoingMessage`
constructor), and was also stricter than Node.js which only checks
`this._header`
- Align the check with the working `setHeader()` (singular) method: only
throw when `_header` is set or `headerStateSymbol` equals `sent`
Closes#27049
## Test plan
- [x] New regression test `test/regression/issue/27049.test.ts` covers:
- `ClientRequest.setHeaders()` with `Headers` object
- `ClientRequest.setHeaders()` with `Map`
- `ServerResponse.setHeaders()` before headers are sent
- [x] Test fails with system bun (`USE_SYSTEM_BUN=1`)
- [x] Test passes with debug build (`bun bd test`)
- [x] Existing header-related tests in `node-http.test.ts` still pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
## Summary
- Compute SHA-512 hash of GitHub tarball bytes during extraction and
store in `bun.lock`
- Verify the hash on subsequent installs when re-downloading, rejecting
tampered tarballs
- Automatically upgrade old lockfiles without integrity by computing and
persisting the hash
- Maintain backward compatibility with old lockfile format (no integrity
field)
Fixes GHSA-pfwx-36v6-832x
## Lockfile format change
```
Before: ["pkg@github:user/repo#ref", {}, "resolved-commit"]
After: ["pkg@github:user/repo#ref", {}, "resolved-commit", "sha512-..."]
```
The integrity field is optional for backward compatibility. Old
lockfiles are automatically upgraded when the tarball is re-downloaded.
## Test plan
- [x] Fresh install stores SHA-512 integrity hash in lockfile
- [x] Re-install with matching hash succeeds
- [x] Re-install with mismatched hash rejects the tarball
- [x] Old lockfile without integrity is auto-upgraded with hash on
re-download
- [x] Cache hits still work without re-downloading
- [x] Existing GitHub dependency tests pass (10/10)
- [x] Existing git resolution snapshot test passes
- [x] Yarn migration snapshot tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
## Summary
- Fixes the esbuild migration guide (`docs/bundler/esbuild.mdx`) which
incorrectly stated that `onStart`, `onEnd`, `onDispose`, and `resolve`
were all unimplemented. `onStart` and `onEnd` **are** implemented — only
`onDispose` and `resolve` remain unimplemented.
- Adds missing `onEnd()` documentation section to both
`docs/bundler/plugins.mdx` and `docs/runtime/plugins.mdx`, including
type signature, description, and usage examples.
- Adds `onEnd` to the type reference overview and lifecycle hooks list
in both plugin docs.
Fixes#27083
## Test plan
- Documentation-only change — no code changes.
- Verified the `onEnd` implementation exists in
`src/js/builtins/BundlerPlugin.ts` and matches the documented API.
- Verified `onStart` implementation exists and is fully functional.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- When `http.ClientRequest.write()` was called more than once (streaming
data in chunks), Bun was stripping the explicitly-set `Content-Length`
header and switching to `Transfer-Encoding: chunked`. Node.js preserves
`Content-Length` in all cases when it's explicitly set by the user.
- This caused real-world failures (e.g. Vercel CLI file uploads) where
large binary files streamed via multiple `write()` calls had their
Content-Length stripped, causing server-side "invalid file size" errors.
- The fix preserves the user's explicit `Content-Length` for streaming
request bodies and skips chunked transfer encoding framing when
`Content-Length` is set.
Closes#27061Closes#26976
## Changes
- **`src/http.zig`**: When a streaming request body has an explicit
`Content-Length` header set by the user, use that instead of adding
`Transfer-Encoding: chunked`. Added
`is_streaming_request_body_with_content_length` flag to track this.
- **`src/bun.js/webcore/fetch/FetchTasklet.zig`**: Skip chunked transfer
encoding framing (`writeRequestData`) and the chunked terminator
(`writeEndRequest`) when the request has an explicit `Content-Length`.
- **`test/regression/issue/27061.test.ts`**: Regression test covering
multiple write patterns (2x write, write+end(data), 3x write) plus
validation that chunked encoding is still used when no `Content-Length`
is set.
## Test plan
- [x] New regression test passes with `bun bd test
test/regression/issue/27061.test.ts`
- [x] Test fails with `USE_SYSTEM_BUN=1` (confirms the bug exists in
current release)
- [x] Existing `test/js/node/http/` tests pass (no regressions)
- [x] Fetch file upload tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
### Problem
The bundler's `__toESM` helper creates a new getter-wrapped proxy object
every time a CJS
module is imported. In a large app, a popular dependency like React can
be imported 600+
times — each creating a fresh object with ~44 getter properties. This
produces ~27K
unnecessary `GetterSetter` objects, ~25K closures, and ~25K
`JSLexicalEnvironment` scope
objects at startup.
Additionally, `__export` and `__exportValue` use `var`-scoped loop
variables captured by
setter closures, meaning all setters incorrectly reference the last
iterated key (a latent
bug).
### Changes
1. **`__toESM`: add WeakMap cache** — deduplicate repeated wrappings of
the same CJS
module. Two caches (one per `isNodeMode` value) to handle both import
modes correctly.
2. **Replace closures with `.bind()`** — `() => obj[key]` becomes
`__accessProp.bind(obj,
key)`. BoundFunction is cheaper than Function + JSLexicalEnvironment,
and frees the for-in
`JSPropertyNameEnumerator` from the closure scope.
3. **Fix var-scoping bug in `__export`/`__exportValue`** — setter
closures captured a
shared `var name` and would all modify the last iterated key. `.bind()`
eagerly captures
the correct key per iteration.
4. **`__toCommonJS`: `.map()` → `for..of`** — eliminates throwaway array
allocation.
5. **`__reExport`: single `getOwnPropertyNames` call** — was calling it
twice when
`secondTarget` was provided.
### Impact (measured on a ~23MB single-bundle app with 600+ React
imports)
| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| **Total objects** | 745,985 | 664,001 | **-81,984 (-11%)** |
| **Heap size** | 115 MB | 111 MB | **-4 MB** |
| GetterSetter | 34,625 | 13,428 | -21,197 (-61%) |
| Function | 221,302 | 197,024 | -24,278 (-11%) |
| JSLexicalEnvironment | 70,101 | 44,633 | -25,468 (-36%) |
| Structure | 40,254 | 39,762 | -492 |
…int.generate()
`ServerEntryPoint.source` defaults to `undefined`, and accessing its
`.contents` or `.path.text` fields before `generate()` has been called
causes a segfault. This happens when `bun:main` is resolved in contexts
where `entry_point.generate()` is skipped (HTML entry points) or never
called (test runner).
Add a `generated` flag to `ServerEntryPoint` and guard both access
sites:
- `getHardcodedModule()` in ModuleLoader.zig (returns null instead of
crashing)
- `_resolve()` in VirtualMachine.zig (falls through to normal
resolution)
### What does this PR do?
### How did you verify your code works?
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
The SIMD fast path in `findEscapeCharacter` matches bytes in `0x10-0x1F`
and `0x90-0x9F`, but `consumeANSI` only handles actual ANSI escape
introducers (`0x1b`, `0x9b`, `0x9d`, `0x90`, `0x98`, `0x9e`, `0x9f`).
For non-ANSI bytes like `0x16` (SYN), `consumeANSI` returns the same
pointer it received, causing an infinite loop in release builds and an
assertion failure in debug builds.
When a SIMD false positive is detected (`consumeANSI` returns the same
pointer), the byte is preserved in the output and scanning advances past
it.
Fixes#27014
## Summary
- Fixes HTTP response splitting vulnerability where `res.statusMessage`
could contain CRLF characters that were written directly to the socket,
allowing injection of arbitrary HTTP headers and response body
- Adds native-layer validation in `NodeHTTPResponse.zig` `writeHead()`
to reject status messages containing control characters (matching
Node.js's `checkInvalidHeaderChar` behavior)
- The `writeHead(code, msg)` API already validated via JS-side
`checkInvalidHeaderChar`, but direct property assignment
(`res.statusMessage = userInput`) followed by `res.end()` or
`res.write()` bypassed all validation
## Test plan
- [x] Verified vulnerability is reproducible: attacker can inject
`Set-Cookie` headers via `res.statusMessage = "OK\r\nSet-Cookie:
admin=true"`
- [x] Verified fix throws `ERR_INVALID_CHAR` TypeError when CRLF is
present in status message
- [x] Added 4 new tests covering: property assignment + `res.end()`,
property assignment + `res.write()`, explicit `writeHead()` rejection,
and valid status message passthrough
- [x] Tests fail with `USE_SYSTEM_BUN=1` (confirming they detect the
vulnerability) and pass with `bun bd test`
- [x] Existing Node.js compat test
`test-http-status-reason-invalid-chars.js` still passes
- [x] All 14 HTTP security tests pass
- [x] Full `node-http.test.ts` suite passes (77 pass, 1 pre-existing
skip, 1 pre-existing proxy failure)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- **HTMLRewriter test**: Replaced `fetch("https://www.example.com/")`
with a local HTTP content server, eliminating dependency on external
HTTPS and system CA certificates
- **HTTPS agent test**: Replaced `https.request("https://example.com/")`
with a local TLS server using self-signed certs from harness
- **Expired certs**: Regenerated self-signed certificates in
`test/js/bun/http/fixtures/` and `test/regression/fixtures/` (were
expired since Sep 2024, now valid until 2036)
## Root cause
Tests fetching external HTTPS URLs (`https://example.com`,
`https://www.example.com`) fail on CI environments (Alpine Linux,
Windows) that lack system CA certificate bundles, producing
`UNABLE_TO_GET_ISSUER_CERT_LOCALLY` errors. This affected:
- `test/js/workerd/html-rewriter.test.js` (HTMLRewriter: async
replacement using fetch + Bun.serve)
-
`test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts`
## Test plan
- [x] `bun bd test test/js/workerd/html-rewriter.test.js` - 41 pass, 0
fail
- [x] `bun bd test test/js/workerd/html-rewriter.test.js -t "async
replacement using fetch"` - 1 pass
- [x] `bun bd
test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts`
- exits 0
- [x] `bun bd test test/js/bun/http/serve-listen.test.ts` - 27 pass, 0
fail (uses renewed certs)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fixes use-after-free (ASAN use-after-poison) when shell builtins
(`ls`, `touch`, `mkdir`, `cp`) run inside command substitution `$(...)`
and encounter errors (e.g., permission denied)
- The `output_waiting` and `output_done` counters in the builtin exec
state went out of sync because `output_waiting` was only incremented for
async IO operations, while `output_done` was always incremented
- In command substitution, stdout is `.pipe` (sync) and stderr is `.fd`
(async), so a single OutputTask completing both writes could satisfy the
done condition while another OutputTask's async stderr write was still
pending in the IOWriter
The fix moves `output_waiting += 1` before the `needsIO()` check in all
four affected builtins so it's always incremented, matching
`output_done`.
## Test plan
- [x] `echo $(ls /tmp/*)` — no ASAN errors (previously crashed with
use-after-poison)
- [x] `echo $(touch /root/a /root/b ...)` — no ASAN errors
- [x] `echo $(mkdir /root/a /root/b ...)` — no ASAN errors
- [x] `ls /nonexistent` — still reports error and exits with code 1
- [x] `echo $(ls /tmp)` — still captures output correctly
- [x] Existing shell test suite: 292 pass, 52 fail (pre-existing), 83
todo — no regressions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
### What does this PR do?
The `sendPong` fix alone wasn't sufficient. The bug only manifests with
**wss:// through HTTP proxy** (not ws://), because only that path uses
`initWithTunnel` with a detached socket.
**Two bugs were found and fixed:**
1. **`sendPong`/`sendCloseWithBody` socket checks**
(`src/http/websocket_client.zig`): Replaced `socket.isClosed() or
socket.isShutdown()` with `!this.hasTCP()` as originally proposed. Also
guarded `shutdownRead()` against detached sockets.
2. **Spurious 1006 during clean close** (`src/http/websocket_client.zig`
+ `WebSocketProxyTunnel.zig`): When `sendCloseWithBody` calls
`clearData()`, it shuts down the proxy tunnel. The tunnel's `onClose`
callback was calling `ws.fail(ErrorCode.ended)` which dispatched a 1006
abrupt close, overriding the clean 1000 close already in progress. Fixed
by adding `tunnel.clearConnectedWebSocket()` before `tunnel.shutdown()`
so the callback is a no-op.
### How did you verify your code works?
- `USE_SYSTEM_BUN=1`: Fails with `Unexpected close code: 1006`
- `bun bd test`: Passes with clean 1000 close
- Full proxy test suite: 25 pass, 4 skip, 0 fail
- Related fragmented/close tests: all passing
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## What
Replace two-step `Vector<uint8_t>` zero-initialization + `memcpy` with
direct `std::span` copy construction in the `structuredClone` Int32 and
Double array fast paths.
## Why
The previous code allocated a zero-filled buffer and then immediately
overwrote it with `memcpy`. By constructing the `Vector` directly from a
`std::span`, we eliminate the redundant zero-fill and reduce the
operation to a single copy.
### Before
```cpp
Vector<uint8_t> buffer(byteSize, 0);
memcpy(buffer.mutableSpan().data(), data, byteSize);
```
### After
```cpp
Vector<uint8_t> buffer(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(data), byteSize });
```
## Test
`bun bd test test/js/web/structured-clone-fastpath.test.ts` — 91/92 pass
(1 pre-existing flaky memory test unrelated to this change).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Defer resolution of dynamic `import()` of unknown `node:` modules
(like `node:sqlite`) to runtime instead of failing at transpile time
- Fix use-after-poison in `addResolveError` by always duping `line_text`
from the source so Location data outlives the arena
Fixes#25707
## Root cause
When a CJS file is `require()`d, Bun's linker eagerly resolves all
import records, including dynamic `import()` expressions. For unknown
`node:` prefixed modules, `whenModuleNotFound` was only deferring
`.require` and `.require_resolve` to runtime — `.dynamic` imports fell
through to the error path, causing the entire `require()` to fail.
This broke Next.js builds with turbopack + `cacheComponents: true` +
Better Auth, because Kysely's dialect detection code uses
`import("node:sqlite")` inside a try/catch that gracefully handles the
module not being available.
Additionally, when the resolve error was generated, the
`Location.line_text` was a slice into arena-allocated source contents.
The arena is reset before `processFetchLog` processes the error, causing
a use-after-poison when `Location.clone` tries to dupe the freed memory.
## Test plan
- [x] New regression test in `test/regression/issue/25707.test.ts` with
3 cases:
- CJS require of file with `import("node:sqlite")` inside try/catch
(turbopack pattern)
- CJS require of file with bare `import("node:sqlite")` (no try/catch)
- Runtime error produces correct `ERR_UNKNOWN_BUILTIN_MODULE` code
- [x] Test fails with `USE_SYSTEM_BUN=1` (system bun v1.3.9)
- [x] Test passes with `bun bd test`
- [x] No ASAN use-after-poison crash on debug build
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
## Summary
- Fix out-of-bounds read in the INI parser's `prepareStr` function when
a multi-byte UTF-8 lead byte appears at the end of a value with
insufficient continuation bytes
- Fix undefined behavior when bare continuation bytes (0x80-0xBF) cause
`utf8ByteSequenceLength` to return 0, hitting an `unreachable` branch
(UB in ReleaseFast builds)
- Add bounds checking before accessing `val[i+1]`, `val[i+2]`,
`val[i+3]` in both escaped and non-escaped code paths
The vulnerability could be triggered by a crafted `.npmrc` file
containing truncated UTF-8 sequences. In release builds, this could
cause OOB heap reads (potential info leak) or undefined behavior.
## Test plan
- [x] Added 9 tests covering truncated 2/3/4-byte sequences, bare
continuation bytes, and escaped contexts
- [x] All 52 INI parser tests pass (`bun bd test
test/js/bun/ini/ini.test.ts`)
- [x] No regressions in npmrc tests (failures are pre-existing Verdaccio
connectivity issues)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Reject null bytes in `username`, `password`, `database`, and `path`
connection parameters for both PostgreSQL and MySQL to prevent wire
protocol parameter injection
- Both the Postgres and MySQL wire protocols use null-terminated strings
in their startup/handshake messages, so embedded null bytes in these
fields act as field terminators, allowing injection of arbitrary
protocol parameters (e.g. `search_path` for schema hijacking)
- The fix validates these fields immediately after UTF-8 conversion and
throws `InvalidArguments` error with a clear message if null bytes are
found
## Test plan
- [x] New test
`test/regression/issue/postgres-null-byte-injection.test.ts` verifies:
- Null bytes in username are rejected with an error before any data is
sent
- Null bytes in database are rejected with an error before any data is
sent
- Null bytes in password are rejected with an error before any data is
sent
- Normal connections without null bytes still work correctly
- [x] Test verified to fail with `USE_SYSTEM_BUN=1` (unfixed bun) and
pass with `bun bd test` (fixed build)
- [x] Existing SQL tests pass (`adapter-env-var-precedence.test.ts`,
`postgres-stringbuilder-assertion-aggressive.test.ts`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fix double-free in `Bun.plugin.clearAll()` by setting `virtualModules
= nullptr` after `delete`
- In `jsFunctionBunPluginClear` (`BunPlugin.cpp:956`), `delete
global->onLoadPlugins.virtualModules` freed the pointer without
nullifying it. When the `OnLoad` destructor later runs (during Worker
termination or VM destruction), it checks `if (virtualModules)` — the
dangling non-null pointer passes the check and is deleted again,
corrupting the heap allocator.
## Test plan
- [ ] New test
`test/regression/issue/plugin-clearall-double-free.test.ts` spawns a
subprocess that registers a virtual module, calls
`Bun.plugin.clearAll()`, and exits with `BUN_DESTRUCT_VM_ON_EXIT=1` to
trigger the destructor path
- [ ] Verified the test fails on the system bun (pre-fix) with `pas
panic: deallocation did fail ... Alloc bit not set`
- [ ] Verified the test passes with the debug build (post-fix)
- [ ] Existing plugin tests (`test/js/bun/plugin/plugins.test.ts`) all
pass (29/29)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
## Summary
- Fix WebSocket client pong frame handler to properly handle payloads
split across TCP segments, preventing frame desync that could cause
protocol confusion
- Add missing RFC 6455 Section 5.5 validation: control frame payloads
must not exceed 125 bytes (pong handler lacked this check, unlike ping
and close handlers)
## Details
The pong handler (lines 652-663) had two issues:
1. **Frame desync on fragmented delivery**: When a pong payload was
split across TCP segments (`data.len < receive_body_remain`), the
handler consumed only the available bytes but unconditionally reset
`receive_state = .need_header` and `receive_body_remain = 0`. The
remaining payload bytes in the next TCP delivery were then
misinterpreted as WebSocket frame headers.
2. **Missing payload length validation**: Unlike the ping handler (line
615) and close handler (line 680), the pong handler did not validate the
7-bit payload length against the RFC 6455 limit of 125 bytes for control
frames.
The fix models the pong handler after the existing ping handler pattern:
track partial delivery state with a `pong_received` boolean, buffer
incoming data into `ping_frame_bytes`, and only reset to `need_header`
after the complete payload has been consumed.
## Test plan
- [x] New test `websocket-pong-fragmented.test.ts` verifies:
- Fragmented pong delivery (50-byte payload split into 2+48 bytes) does
not cause frame desync, and a subsequent text frame is received
correctly
- Pong frames with >125 byte payloads are rejected as invalid control
frames
- [x] Test fails with `USE_SYSTEM_BUN=1` (reproduces the bug) and passes
with `bun bd test`
- [x] Existing WebSocket tests pass: `websocket-client.test.ts`,
`websocket-close-fragmented.test.ts`,
`websocket-client-short-read.test.ts`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fixes HTTP header injection vulnerability in S3 client where
user-controlled options (`contentDisposition`, `contentEncoding`,
`type`) were passed to HTTP headers without CRLF validation
- Adds input validation at the JS-to-Zig boundary in
`src/s3/credentials.zig` that throws a `TypeError` if `\r` or `\n`
characters are detected
- An attacker could previously inject arbitrary headers (e.g.
`X-Amz-Security-Token`) by embedding `\r\n` in these string fields
## Test plan
- [x] Added `test/regression/issue/s3-header-injection.test.ts` with 6
tests:
- CRLF in `contentDisposition` throws
- CRLF in `contentEncoding` throws
- CRLF in `type` (content-type) throws
- Lone CR in `contentDisposition` throws
- Lone LF in `contentDisposition` throws
- Valid `contentDisposition` without CRLF still works correctly
- [x] Tests fail with `USE_SYSTEM_BUN=1` (confirming vulnerability
exists in current release)
- [x] Tests pass with `bun bd test` (confirming fix works)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fix path traversal vulnerability in tarball directory extraction on
POSIX systems where `mkdiratZ` used the un-normalized `pathname` (raw
from tarball) instead of the normalized `path` variable, allowing `../`
components to escape the extraction root via kernel path resolution
- The Windows directory creation, symlink creation, and file creation
code paths already correctly used the normalized path — only the two
POSIX `mkdiratZ` calls were affected (lines 463 and 469)
- `bun install` is not affected because npm mode skips directory
entries; affected callers include `bun create`, GitHub tarball
extraction, and `compile_target`
## Test plan
- [x] Added regression test that crafts a tarball with
`safe_dir/../../escaped_dir/` directory entry and verifies it cannot
create directories outside the extraction root
- [x] Verified test **fails** with system bun (vulnerable) and
**passes** with debug build (fixed)
- [x] Full `archive.test.ts` suite passes (99/99 tests)
- [x] `symlink-path-traversal.test.ts` continues to pass (3/3 tests)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Replace `bun.strings.eqlLong` with BoringSSL's `CRYPTO_memcmp` for
SCRAM-SHA-256 server signature verification in the PostgreSQL client
- The previous comparison (`eqlLong`) returned early on the first
mismatching byte, potentially leaking information about the expected
server signature via timing side-channel
- `CRYPTO_memcmp` is already used elsewhere in the codebase for
constant-time comparisons (CSRF tokens, `crypto.timingSafeEqual`,
KeyObject comparison)
## Test plan
- [x] `bun bd` compiles successfully
- [ ] Existing SCRAM-SHA-256 integration tests in
`test/js/sql/sql.test.ts` pass (require Docker/PostgreSQL)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fixes#26918 — segfault at address `0x28189480080` caused by
use-after-free in the shell interpreter
- When `setupIOBeforeRun()` fails (e.g., stdout handle unavailable on
Windows), the `runFromJS` error path called `deinitFromExec()` which
directly freed the GC-managed interpreter object with
`allocator.destroy(this)`. When the GC later swept and called
`deinitFromFinalizer()` on the already-freed memory, it caused a
segfault.
- Replaced `deinitFromExec()` with `derefRootShellAndIOIfNeeded(true)`
which properly cleans up runtime resources (IO handles, shell
environment) while leaving final object destruction to the GC finalizer
— matching the pattern already used in `finish()`.
## Test plan
- [x] Added regression test in `test/regression/issue/26918.test.ts`
that verifies the shell interpreter handles closed stdout gracefully
without crashing
- [x] Test passes with `bun bd test test/regression/issue/26918.test.ts`
- [ ] The actual crash is primarily reproducible on Windows where stdout
handles can be truly unavailable — CI Windows tests should validate the
fix
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
## Summary
- When `--bail` caused an early exit after a test failure, the JUnit
reporter output file (`--reporter-outfile`) was never written because
`Global.exit()` was called before the normal completion path
- Extracted the JUnit write logic into a `writeJUnitReportIfNeeded()`
method on `CommandLineReporter` and call it in both bail exit paths
(test failure and unhandled rejection) as well as the normal completion
path
Closes#26851
## Test plan
- [x] Added regression test `test/regression/issue/26851.test.ts` with
two cases:
- Single failing test file with `--bail` produces JUnit XML output
- Multiple test files where bail triggers on second file still writes
the report
- [x] Verified test fails with system bun (`USE_SYSTEM_BUN=1`)
- [x] Verified test passes with `bun bd test`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Add `expectMessageEventually(value)` to the bake test harness `Client`
class — waits for a specific message to appear, draining any
intermediate messages that arrived before it
- Rewrite "hmr handles rapid consecutive edits" test to use raw
`Bun.write` + sleep for intermediate edits and `expectMessageEventually`
for the final assertion, avoiding flaky failures when HMR batches
updates non-deterministically across platforms
Fixes flaky failure on Windows where an extra "render 10" message
arrived after `expectMessage` consumed its expected messages but before
client disposal.
## Test plan
- [x] `bun bd test test/bake/dev-and-prod.test.ts` — all 12 tests pass
- [x] Ran the specific test multiple times to confirm no flakiness
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Alistair Smith <alistair@anthropic.com>
## Problem
The bundler's number renamer was mangling `.name` properties on crypto
class prototype methods and constructors:
- `hash.update.name` → `"update2"` instead of `"update"`
- `verify.verify.name` → `"verify2"` instead of `"verify"`
- `cipher.update.name` → `"update3"` instead of `"update"`
- `crypto.Hash.name` → `"Hash2"` instead of `"Hash"`
### Root causes
1. **Named function expressions on prototypes** collided with other
bindings after scope flattening (e.g. `Verify.prototype.verify =
function verify(...)` collided with the imported `verify`)
2. **Block-scoped constructor declarations** (`Hash`, `Hmac`) got
renamed when the bundler hoisted them out of block scope
3. **Shared function declarations** in the Cipher/Decipher block all got
numeric suffixes (`update3`, `final2`, `setAutoPadding2`, etc.)
## Fix
- Use `Object.assign` with object literals for prototype methods —
object literal property keys correctly infer `.name` and aren't subject
to the renamer
- Remove unnecessary block scopes around `Hash` and `Hmac` constructors
so they stay at module level and don't get renamed
- Inline `Cipheriv` methods and copy references to `Decipheriv`
## Tests
Added comprehensive `.name` tests for all crypto classes: Hash, Hmac,
Sign, Verify, Cipheriv, Decipheriv, DiffieHellman, ECDH, plus factory
functions and constructor names.
## What does this PR do?
Fixes the write loop in `StandaloneModuleGraph.inject()` for POSIX
targets (the `else` branch handling ELF/Linux standalone binaries) to
pass `remain` instead of `bytes` to `Syscall.write()`.
## Problem
The write loop that appends the bundled module graph to the end of the
executable uses a standard partial-write retry pattern, but passes the
full `bytes` buffer on every iteration instead of the remaining portion:
```zig
var remain = bytes;
while (remain.len > 0) {
switch (Syscall.write(cloned_executable_fd, bytes)) { // bug: should be 'remain'
.result => |written| remain = remain[written..],
...
}
}
```
If a partial write occurs, the next iteration re-writes from the start
of the buffer instead of continuing where it left off, corrupting the
output binary. The analogous read loop elsewhere in the same file
already correctly uses `remain`.
## Fix
One-character change: `bytes` → `remain` in the `Syscall.write()` call.
## How did you verify your code works?
- `bun bd` compiles successfully
- `bun bd test test/bundler/bun-build-compile.test.ts` — 4/4 pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <alistair@anthropic.com>
## What does this PR do?
Fixes `bun build --compile` producing an all-zeros binary when the
output directory is on a different filesystem than the temp directory.
This is common in Docker containers, Gitea runners, and other
environments using overlayfs.
## Problem
When `inject()` finishes writing the modified executable to the temp
file, the file descriptor's offset is at EOF. If the subsequent
`renameat()` to the output path fails with `EXDEV` (cross-device — the
temp file and output dir are on different filesystems), the code falls
back to `copyFileZSlowWithHandle()`, which:
1. Calls `fallocate()` to pre-allocate the output file to the correct
size (filled with zeros)
2. Calls `bun.copyFile(in_handle, out_handle)` — but `in_handle`'s
offset is at EOF
3. `copy_file_range` / `sendfile` / `read` all use the current file
offset (EOF), read 0 bytes, and return immediately
4. Result: output file is the correct size but entirely zeros
This explains user reports of `bun build --compile
--target=bun-darwin-arm64` producing invalid binaries that `file`
identifies as "data" rather than a Mach-O executable.
## Fix
Seek the input fd to offset 0 in `copyFileZSlowWithHandle` before
calling `bun.copyFile`.
## How did you verify your code works?
- `bun bd` compiles successfully
- `bun bd test test/bundler/bun-build-compile.test.ts` — 6/6 pass
- Added tests that verify compiled binaries have valid executable
headers and produce correct output
- Manually verified cross-compilation: `bun build --compile
--target=bun-darwin-arm64` produces a valid Mach-O binary
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Update inline error snapshots in valkey reliability tests to match
Redis 8's changed error message format
- Redis 8 (`redis:8-alpine` in our test Docker container) no longer
appends `, with args beginning with: ` when an unknown command has no
arguments
## Root cause
Redis commit
[`25f780b6`](25f780b662)
([PR #14690](https://github.com/redis/redis/pull/14690)) changed
`commandCheckExistence()` in `src/server.c` to only append `, with args
beginning with: ` when there are actual arguments (`c->argc >= 2`).
Previously it was always appended, producing a dangling `, with args
beginning with: ` even with zero arguments.
## Changes
- `test/js/valkey/reliability/protocol-handling.test.ts`: Updated
`SYNTAX-ERROR` snapshot (no args case)
- `test/js/valkey/reliability/error-handling.test.ts`: Updated
`undefined` and `123` snapshots (no args cases)
## Test plan
- [ ] Verify `protocol-handling.test.ts` passes in CI (was failing on
every attempt as shown in #26869 / build #36831)
- [ ] Verify `error-handling.test.ts` passes in CI
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
#### (Copies commits from #26447)
## Summary
- Add a global `--retry <N>` flag to `bun test` that sets a default
retry count for all tests (overridable by per-test `{ retry: N }`). Also
configurable via `[test] retry = N` in bunfig.toml.
- When a test passes after one or more retries, the JUnit XML reporter
emits a separate `<testcase>` entry for each failed attempt (with
`<failure>`), followed by the final passing `<testcase>`. This gives
flaky test detection tools per-attempt timing and result data using
standard JUnit XML that all CI systems can parse.
## Test plan
- `bun bd test test/js/junit-reporter/junit.test.js` — verifies separate
`<testcase>` entries appear in JUnit XML for tests that pass after retry
- `bun bd test test/cli/test/retry-flag.test.ts` — verifies the
`--retry` CLI flag applies a default retry count to all tests
## Changelog
<!-- CHANGELOG:START -->
- Added `--retry <N>` flag to `bun test` to set a default retry count
for all tests
- Added `[test] retry` option to bunfig.toml
- JUnit XML reporter now emits separate `<testcase>` entries for each
retry attempt, providing CI visibility into flaky tests
<!-- CHANGELOG:END -->
---------
Co-authored-by: Chris Lloyd <chrislloyd@anthropic.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
Fixes "Unknown HMR script" error during rapid consecutive edits in HMR
## Test plan
- [x] Basic consecutive HMR edits work correctly
---------
Co-authored-by: Alistair Smith <alistair@anthropic.com>
## Summary
- Convert `file:` URL strings to filesystem paths via
`Bun.fileURLToPath()` in the JS layer for both `fs.watch` and
`fs.watchFile`/`fs.unwatchFile`
- Handles percent-decoding (e.g. `%20` → space) and proper URL parsing,
which the previous naive `slice[6..]` stripping in Zig could not do
- Zig-level `file://` stripping is left unchanged; the JS layer now
resolves file URLs before they reach native code
## Test plan
- [x] New test: `fs.watch` with `file:` URL string containing
`%20`-encoded spaces
- [x] New test: `fs.watchFile` with `file:` URL string containing
`%20`-encoded spaces
- [x] Both tests fail with `USE_SYSTEM_BUN=1` and pass with `bun bd
test`
- [x] Existing `fs.watch` "should work with url" test (URL object) still
passes
- [x] Full `fs.watchFile` suite passes (6 pass, 1 skip, 0 fail)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Implement complete lowering of TC39 stage-3 standard ES decorators
(the non-legacy variant used when tsconfig has no
`experimentalDecorators`).
- Passes all 147 esbuild decorator tests and 22 additional Bun-specific
tests (191 total, 0 failures).
- Supports method, getter, setter, field, auto-accessor, private member,
and class decorators in both statement and expression positions, with
proper evaluation order, class binding semantics, and decorator
metadata.
Fixes#4122Fixes#20206Fixes#14529Fixes#6051
## What's implemented
| Feature | Details |
|---|---|
| Method/getter/setter decorators | Static and instance, public and
private |
| Field decorators | Initializer replacement + extra initializers via
`__runInitializers` |
| Auto-accessor (`accessor` keyword) | Lowered to WeakMap storage +
getter/setter pair |
| Private member decorators | WeakMap/WeakSet lowering with
`__privateGet`/`__privateSet` |
| Class decorators | Statement and expression positions |
| Class expression decorators | Comma-expression lowering (no IIFE) |
| Decorator metadata | `Symbol.metadata` support via
`__decoratorMetadata` |
| Evaluation order | All decorator expressions + computed keys evaluated
in source order per TC39 spec |
| Class binding semantics | Separate inner/outer class name bindings
(element vs class decorator closures) |
| Static block extraction | `this` replaced with class name ref when
moved to suffix |
| Computed property keys | Pre-evaluated into temp variables for correct
ordering |
## Runtime helpers
Added to `src/runtime.js` and registered in `src/runtime.zig`:
- `__decoratorStart(base)` — creates decorator context array
- `__decorateElement(array, flags, name, decorators, target, extra)` —
applies decorators to a class element
- `__decoratorMetadata(array, target)` — sets `Symbol.metadata` on the
class
- `__runInitializers(array, flags, self, value)` — runs
initializer/extra-initializer arrays
## Test plan
- [x] `bun bd test
test/bundler/transpiler/es-decorators-esbuild.test.ts` — **147/147
pass** (esbuild's full decorator test suite)
- [x] `bun bd test test/bundler/transpiler/es-decorators.test.ts` —
**22/22 pass**
- [x] `bun bd test test/bundler/transpiler/decorators.test.ts` — **22/22
pass** (legacy decorators still work)
- [x] E2E runtime verification of method, field, accessor, class,
private, and expression decorators
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
## Summary
Adds a **DenseArray fast path** for `structuredClone` / `postMessage`
that completely skips byte-buffer serialization when an
`ArrayWithContiguous` array contains **flat objects** (whose property
values are only primitives or strings).
This builds on #26814 which added fast paths for Int32/Double/Contiguous
arrays of primitives and strings. The main remaining slow case was
**arrays of objects** — the most common real-world pattern (e.g.
`[{name: "Alice", age: 30}, {name: "Bob", age: 25}]`). Previously, these
fell back to the full serialization path because the Contiguous fast
path rejected non-string cell elements.
## How it works
### Serialization
The existing Contiguous array handler is extended to recognize object
elements that pass `isObjectFastPathCandidate` (FinalObject, no
getters/setters, no indexed properties, all enumerable). For qualifying
objects, properties are collected into a `SimpleCloneableObject` struct
(reusing the existing `SimpleInMemoryPropertyTableEntry` type). The
result is stored as a `FixedVector<DenseArrayElement>` where
`DenseArrayElement = std::variant<JSValue, String,
SimpleCloneableObject>`.
If no object elements are found, the existing `SimpleArray` path is used
(no regression).
### Deserialization
A **Structure cache** avoids repeated Structure transitions when the
array contains many same-shape objects (the common case). The first
object is built via `constructEmptyObject` + `putDirect`, and its final
Structure + Identifiers are cached. Subsequent objects with matching
property names are created directly with `JSFinalObject::create(vm,
cachedStructure)`, skipping all transitions.
Safety guards:
- Cache is only used when property count AND all property names match
- Cache is disabled when `outOfLineCapacity() > 0` (properties exceed
`maxInlineCapacity`), since `JSFinalObject::create` cannot allocate a
butterfly
### Fallback conditions
| Condition | Behavior |
|-----------|----------|
| Elements are only primitives/strings | SimpleArray (existing) |
| Elements include `isObjectFastPathCandidate` objects | **DenseArray
(NEW)** |
| Object property value is an object/array | Fallback to normal path |
| Elements include Date, RegExp, Map, Set, ArrayBuffer, etc. | Fallback
to normal path |
| Array has holes | Fallback to normal path |
## Benchmarks
Apple M4 Max, release build vs system Bun v1.3.8 and Node.js v24.12:
| Benchmark | Node.js v24.12 | Bun v1.3.8 | **This PR** | vs Bun | vs
Node |
|-----------|---------------|------------|-------------|--------|---------|
| `[10 objects]` | 2.83 µs | 2.72 µs | **1.56 µs** | **1.7x** | **1.8x**
|
| `[100 objects]` | 24.51 µs | 25.98 µs | **14.11 µs** | **1.8x** |
**1.7x** |
## Test coverage
28 new edge-case tests covering:
- **Property value variants**: empty objects, special numbers (NaN,
Infinity, -0), null/undefined values, empty string keys, boolean-only
values, numeric string keys
- **Structure cache correctness**: alternating shapes, objects
interleaved with primitives, >maxInlineCapacity properties (100+), 1000
same-shape objects (stress test), repeated clone independence
- **Fallback correctness**: array property values, nested objects,
Date/RegExp/Map/Set/ArrayBuffer elements, getters, non-enumerable
properties, `Object.create(null)`, class instances
- **Frozen/sealed**: clones are mutable regardless of source
- **postMessage via MessageChannel**: mixed arrays with objects, empty
object arrays
## Changed files
- `src/bun.js/bindings/webcore/SerializedScriptValue.h` —
`SimpleCloneableObject`, `DenseArrayElement`, `FastPath::DenseArray`,
factory/constructor/member
- `src/bun.js/bindings/webcore/SerializedScriptValue.cpp` — serialize,
deserialize, `computeMemoryCost`
- `test/js/web/structured-clone-fastpath.test.ts` — 28 new tests
- `bench/snippets/structuredClone.mjs` — object array benchmarks
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>