You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Patterns are now inlined as a typed const with Sources derived via keyof typeof patterns. Also runnable as a CLI:
bun scripts/glob-sources.ts cxx # one .cpp path per line
bun scripts/glob-sources.ts --all # every source list
Uses node:fsglobSync so it runs under both Bun and Node. Consumers
updated: configure.ts, run-clang-format.sh, ban-words.test.ts, lldb-inline-tool.cpp, cppbind.ts, sync-webkit-source.ts.
New: bun run clean
bun run clean # build/debug/
bun run clean release # build/release/
bun run clean debug-local # build/debug-local/ + vendor/WebKit/WebKitBuild/Debug
bun run clean zig # zig caches across all profiles
bun run clean cpp # obj/ + pch/ across all profiles
bun run clean deep # build/, vendor/* (except WebKit), zig caches
bun run clean --help
The deep vendor list is derived from allDeps in scripts/build/deps/index.ts, so new dependencies are picked up
automatically.
Docs updated
CONTRIBUTING.md and docs/project/contributing.mdx now point at scripts/build/deps/webkit.ts for WEBKIT_VERSION and the --asan=off
flag for disabling ASAN.
Notes
cmake the tool is still required — vendored deps (zstd, libarchive,
boringssl, etc.) build via nested cmake
scripts/bump.ts was already missing (deleted in an earlier commit);
the bump action and package.json script that reference it are
pre-existing breakage
test/js/bun/util/zstd.test.ts has a snapshot of the old package.json
as compression test data; will need a snapshot regen
1cc837 Enable TCP_DEFER_ACCEPT for HTTP listeners on Linux (#28617)
What
Sets TCP_DEFER_ACCEPT on the listen socket for Bun.serve() (Linux)
and SO_ACCEPTFILTER "dataready" (FreeBSD). When the kernel defers
accept, the accept loop dispatches the socket as readable immediately
instead of returning to epoll first.
Why
For short-lived connections (HTTP/1.1 with Connection: close), the
server currently does:
epoll wake → listen socket readable
accept() → register new socket with epoll
return to epoll ← wasted round-trip
epoll wake → new socket readable
recv() → process → respond → close
With TCP_DEFER_ACCEPT, the kernel holds the accept until data arrives,
so step 3–4 collapses into step 2. Same pattern nginx uses (rev->ready = 1 after deferred
accept).
Works for TLS too — the ClientHello is the first data packet.
Scope
Opt-in via LIBUS_LISTEN_DEFER_ACCEPT, only set by uWS HttpContext::listen
Benchmark oha --disable-keepalive on Linux to quantify
improvement
Co-authored-by: robobun <bot@oven.sh>
44f5b6 Add second cork buffer to uWebSockets for nested async handlers (#28615)
What
Replaces uWebSockets' single shared cork buffer with two independent
cork slots.
Why
When an async HTTP handler awaits and later resumes (e.g. another
request resolves its promise), the resumed request's writeHead() + end() couldn't get the cork buffer because the newer in-flight request
was holding it. This forced the resumed request down the uncorked path —
one write() syscall per call instead of batching them. On the bench/snippets/http-hello.node.mjs interleaved-await pattern this
dropped throughput from ~190k req/s to ~22k req/s.
How
Two independent slots: each holds {buffer, socket, offset, ssl}. cork() grabs whichever slot is free; uncork() releases yours. No
ordering constraints.
Empty-slot stealing: if both slots are claimed but one has offset==0 (corked but not yet written), it's freely stolen — no flush
needed.
LRU eviction: when both slots hold actual data (rare, depth 3+),
force-uncork the least-recently-touched one. Tracked via a single bit.
Entry-point re-cork: NodeHTTP writeHead and HttpResponse::cork(handler) call cork() before each write sequence,
so if your slot was stolen while you were awaiting, you transparently
get a fresh one.
UAF protection: uncorkWithoutSending() (called from all
close/destroy paths) now clears the socket from any slot via unborrowCorkSlot(), preventing the drain loop from dereferencing a
freed pointer.
Bounded drain: preCb and uws_res_clear_corked_socket iterate
at most twice (one per slot).
Memory cost: one extra 16KB buffer per event loop.
Tests
test/js/node/http/node-http-nested-cork.test.ts — 20 concurrent
interleaved-await requests for both node:http and Bun.serve, asserts all
responses are correct with no cross-socket bleed.
serve.test.ts (189 tests), node-http.test.ts (78 tests), websocket-server.test.ts all pass.
7c4714 fix(serve): leak when Promise never settles after abort (#28613)
Summary
When Bun.serve()'s fetch handler returns a pending Promise<Response>
and the client disconnects before it settles, the RequestContext
leaked forever. The ctx.ref() before .then() was only balanced by onResolve/onReject, which never fire for unsettled promises.
This introduces NativePromiseContext — a minimal GC-managed JSCell
that owns the ref:
JSC::JSCell + CellType + custom IsoHeapCellType for
destructor support
CompactPointerTuple<Pointee*, Tag> packs pointer + tag in 8
bytes (cell = JSCell header + 1 word)
Tag enum class with one entry per concrete type (4 RequestContext
variants), kept in sync C++↔Zig
take() transfers ownership for deterministic cleanup in the
normal path
Destructor calls one extern C function that Zig implements with a
tag switch — no function pointers, no Weak<> overhead
If the Promise settles: onResolve calls take() → processes → deref(). Cell destructor later sees null, no-ops.
If the Promise is GC'd without settling: cell collected with its
reaction → destructor → deref(). No leak, no UAF.
Applied to all three .then() callsites in RequestContext: main
response promise, error handler promise, and response body stream
promise.
Test plan
New test: test/js/bun/http/serve-pending-promise-abort-leak.test.ts — 4 tests
covering the leak fix, normal resolution, resolve-in-abort-handler, and
resolve-after-abort-via-setTimeout (UAF safety)
Test fails on system Bun: LEAK: 100 RequestContexts were never freed
Test passes with fix: pendingRequests: 0
heapStats().objectTypeCounts.NativePromiseContext shows 50 for
50 concurrent pending, 0 after resolve+GC
2e610b Fix crash when consuming Response body from async iterable (#28457)
Two issues caused a crash when calling bytes()/arrayBuffer() on a Response whose body was created from an async iterable
(Symbol.asyncIterator):
readableStreamToBytes/readableStreamToArrayBuffer checked underlyingSource !== undefined but initializeArrayBufferStream sets
it to null. Since null !== undefined is true, null was passed to readableStreamToArrayBufferDirect which dereferenced it
(underlyingSource.pull on null). Fixed by using != null to exclude
both null and undefined.
The C++ functions ZigGlobalObject__readableStreamToBytes and ZigGlobalObject__readableStreamToArrayBufferBody did not check for
exceptions after calling the JS builtin via call(). Added RETURN_IF_EXCEPTION after the call to properly propagate exceptions
instead of leaving them unhandled, which triggered releaseAssertNoException in debug builds.
Also added null guards for this.$sink in onCloseDirectStream and onFlushDirectStream, matching the existing pattern in handleDirectStreamError. Stream is now properly locked via a dummy
reader in readableStreamToArrayBufferDirect to prevent a second call
from bypassing the lock check.
Verification (robobun): Lint JS ✅. Diff clean — no TODO/FIXME/HACK.
Builds #41331 and #41340 were canceled (not failed); Build #41343 is
still compiling. Code verified: != null correctly excludes null set at
ReadableStreamInternals.ts:2266; all 6 C++ wrappers now have
DECLARE_THROW_SCOPE + RETURN_IF_EXCEPTION; sink null guards in
onCloseDirectStream/onFlushDirectStream placed before $streamClosing
assignment, matching handleDirectStreamError pattern (line 988-994);
dummy reader {} at line 2268 makes isReadableStreamLocked (line 1573)
return true so a second consumption call is properly rejected. Test
spawns a subprocess with an async-iterable-backed Response, calls
bytes()/arrayBuffer() twice, asserts no "null is not an object" in
stdout — on unfixed bun this message appears because null.pull is
dereferenced. Reviews: claude LGTM, CodeRabbit $streamClosing ordering
concern addressed in code, themavik != null semantics confirmed
intentional. ⚠️ Human reviewer: please confirm Buildkite #41343 is green
before merging.
9ead1e fix: use isObject() instead of isCell() for dns.lookup options check (#28424)
When Bun.dns.lookup() receives a non-object cell (e.g. a string) as
the second argument, the isCell() guard at line 2722 passes, but then getTruthy() calls JSValue.get() which asserts target.isObject(),
causing a crash.
The fix changes the guard from isCell() to isObject() so non-object
cells are properly skipped as options arguments.
Root cause:isCell() returns true for strings and other non-object
JS cells, but JSValue.get() requires the target to be an actual
object.
Repro:
constdns=Bun.dns;dns.lookup("127.0.0.1","cat");
Verification (iteration 6): CI build #41167 pending (Lint JS passed);
previous build #41087 failures were only webview.test.ts (macOS timeout)
and bundler_compile.test.ts (Linux timeout), both unrelated to DNS. Diff
is a single-line guard change isCell()->isObject() in dns.zig and a
regression test that exercises the exact crash path (string arg triggers
debugAssert in JSValue.get on main). All 3 review threads resolved. No
TODO/FIXME/HACK in diff, no unrelated changes.
7fb789 Remove JSCallbackDataStrong, unify into single Weak JSCallbackData (#28539)
What
Follow-up to #28494. Backports WebKit 276563 ("Make all
callbacks Weak handles"), which removed JSCallbackDataStrong/JSCallbackDataWeak in favor of a single
Weak-only JSCallbackData class.
Why
JSC::Strong handles in callback wrappers have been a recurring source
of GC reference cycles (see #28491 for the pipeTo + signal leak).
Upstream WebKit solved this structurally in 2024-07 by deleting the
Strong variant entirely, making such leaks impossible to introduce by
construction.
#28494 deferred this backport because it "requires restructuring AbortSignal::m_algorithms to allow GC visitation." That restructuring
was already done in #28491 (m_abortAlgorithms + visitAbortAlgorithms() + Lock), so the prerequisite is satisfied and
this is now a pure rename + dead code removal.
Changes
JSCallbackData.h / .cpp
Merged JSCallbackDataWeak into the base JSCallbackData class
Deleted JSCallbackDataStrong
Replaced Strong.h/StrongInlines.h includes with Weak.h/WeakInlines.h
Kept Bun's existing invokeCallback(VM&, ...) signature and visitJSFunction method name (not upstream's visitJSFunctionInGCThread) to avoid touching base class overrides —
this is a structural sync, not a full API sync
JSAbortAlgorithm, JSPerformanceObserverCallback
Type rename only: JSCallbackDataWeak* → JSCallbackData*
FileSystemEntry API callback (webkitGetAsEntry() etc.), never
implemented in Bun
Zero create() call sites, zero #include references outside itself
Was the only remaining JSCallbackDataStrong user
Upstream still has ErrorCallback under Modules/entriesapi/, but
with [GenerateIsReachable] which generates a Weak callback — our copy
predated that and was a stale Strong version
# Copy the package.json and bun.lock into the container
COPY package.json bun.lock ./
-# Install the dependencies
# Install the dependencies
RUN bun install --production --frozen-lockfile
```</li><li><a href="https://github.com/oven-sh/bun/commit/0bcb4025d3e3993dd0a77951decbf0b8f934d4f7"><code>0bcb40</code></a> Bun.WebView: EventTarget, screenshot formats, zero-copy mmap Blob, .cdp() (#28362)
## EventTarget inheritance
`Bun.WebView` now extends `EventTarget` —
`addEventListener`/`removeEventListener`/`dispatchEvent` are inherited.
The impl side (`WebViewEventTarget`) is a thin wrapper holding just the
listener map; all WebView state stays on the JS cell.
## screenshot({format, quality}) returning zero-copy mmap-backed Blob
```ts
const png = await view.screenshot(); // Blob, image/png
const jpeg = await view.screenshot({ format: "jpeg", quality: 90 });
const webp = await view.screenshot({ format: "webp" }); // chrome-only
WebKit: zero-copy — the child writes encoded image bytes to a POSIX
shm segment; the parent mmap's it directly into the Blob's backing
store. The mapping is released when the Blob is garbage-collected. await blob.bytes() reads straight from the mapped pages.
Chrome: decodes the CDP base64 response and copies into a
mimalloc-owned buffer (the base64 decode allocation is unavoidable).
format: "webp" requires the Chrome backend (NSBitmapImageRep has no
WebP encoding; the ImageIO CGImageDestination path with public.webp
UTI is deferred).
.cdp(method, params?) — raw CDP escape hatch (Chrome-only)
abe02a glob: fix undeclared component_idx after active-set refactor (#28543)
What
Fixes a build error on main introduced by the interaction of #28496
and #28489:
src/glob/GlobWalker.zig:715:69: error: use of undeclared identifier 'component_idx'
iterator.setNameFilter(this.computeNtFilter(component_idx));
^~~~~~~~~~~~~
Why
#28496 replaced the single component_idx variable with an active
BitSet to track multiple active pattern component indices. #28489
(merged just before) added a Windows NtQueryDirectoryFile filter call
that references component_idx. After both landed, the variable no
longer exists in scope.
Fix
computeNtFilter operates on a single pattern component, so it can only
be applied when exactly one index is active. For multi-index states
(e.g. after **), a single-component kernel filter could hide entries
that other active indices need to match, so we skip it.
This is safe because the NT filter is purely a pre-filter optimization — matchPatternImpl still runs on every returned entry for correctness
(per the existing doc comment on computeNtFilter).
Verification
bun run zig:check-all passes on all platforms (macOS/Linux/Windows ×
x86_64/aarch64 × Debug/Release).
390948 fix: don't throw new exception when termination exception is pending (#28535)
When a stack overflow (termination exception) occurs during error
message formatting in throw/throwPretty, createErrorInstance
returns .zero and the exception remains pending. Previously this would
either hit bun.assert(instance != .zero) or reach VM.throwError
which calls releaseAssertNoException, crashing the process.
Guard throw, throwPretty, and throwValue to return error.JSError
early when an exception is already pending on the VM.
Crash fingerprint: e9adb7008f7e2bd5
698eb8 Fix assert.partialDeepStrictEqual crashing on array inputs (#28525)
Fixes #28522
Repro:
importassertfrom"node:assert/strict";assert.partialDeepStrictEqual(["foo"],["foo"]);// TypeError: expectedCounts.@​set is not a function
Cause:SafeMap instances have their prototype set to null by makeSafe(), which breaks JSC private method resolution (.$set, .$delete). The array comparison branch in compareBranch used expectedCounts.$set() and expectedCounts.$delete() directly, which
require the prototype chain to be intact.
Fix: Extract uncurried SafeMapPrototypeSet / SafeMapPrototypeDelete references (matching the existing pattern for SafeMapPrototypeGet and SafeMapPrototypeHas) and call them with .$call().
Verification:
USE_SYSTEM_BUN=1 bun test test/regression/issue/28522.test.ts →
fails (bug present)
bun bd test test/regression/issue/28522.test.ts → passes (fix works)
Co-authored-by: Alistair Smith <hi@alistair.sh>
e59a14 perf(glob): track active component indices as a set to eliminate double-visits (#28496)
What
Eliminates redundant directory visits in Bun.Glob.scan() for patterns
containing **/X/.... Directories under the boundary were being opened
and read twice; they're now read once.
Why
A glob pattern like **/node_modules/**/*.js is split into components:
idx: 0 1 2 3
** node_modules ** *.js
When the walker is at ** (idx 0) and encounters a directory named node_modules, there are two valid interpretations:
Advance: this is the node_modules we were looking for — jump to
idx 2 and match *.js inside.
Keep going: ** matches zero or more dirs, so node_modules
might
just be another directory under **. Stay at idx 0 in case there's
another node_modules deeper.
Both are correct and both can produce matches. The previous
implementation handled this by pushing two WorkItems for the same
path — one at idx 0, one at idx 2. Each triggered its own openat + readdir + close, and since both recurse, every descendant of
the boundary was read twice.
How
Instead of forking the traversal, carry a set of active component
indices per WorkItem. When a boundary is crossed, both indices go into
one set; the directory is iterated once, and each entry is checked
against all active positions. The child's set is the union of what each
active index says to do next.
Additionally, when the component after a **/X boundary is itself **
(as in **/node_modules/**/*.js), the outer ** is dropped entirely —
the inner one's recursion already covers everything the outer one would
find.
This is the standard NFA state-set simulation, applied to filesystem
traversal.
Benchmarks
**/node_modules/**/*.js with { dot: true } on the next.js repo
(68,233 matches, macOS arm64):
Gain scales with how much of the tree is under the **/X boundary.
Synthetic trees with deeper nesting hit 2.9×. Patterns without a
boundary (**/*.ts) are unchanged.
Implementation
ComponentSet is an alias for bun.bit_set.AutoBitSet, which stores
up to 127 indices inline in [2]usize and spills to heap beyond —
patterns of any depth stay correct.
Added count(), findFirstSet(), and iterator() dispatch methods
to AutoBitSet.
evalDir/evalFile/evalImpl replace the inline per-index logic;
they iterate the active set and accumulate results.
Net +35 lines (the set helpers collapsed triplicated bump-handling
blocks in the old code).
Added a test with 130-component patterns to cover the heap-backed
path.
3ca678 fix index out of bounds in braces lexer on empty input (#28487)
When Bun.$.braces("") is called with an empty string, the tokenizer
produces zero tokens. flattenTokens then accesses self.tokens.items[0] unconditionally, causing an index-out-of-bounds
panic. Additionally, Parser.advance() unconditionally calls prev()
which underflows when current == 0.
The fix adds an early return when the token list is empty in flattenTokens, and guards advance() to return peek() instead of
underflowing prev() when current == 0.
Verification (robobun, iteration 4): Build #41524 (commit 7045ab1)
completed — 5 failing tests are all unrelated (bun-upgrade, issue/8254,
webview timeout, issue/24364, bun-types), none in shell/braces. Build
#41555 (CI retry commit 477200b, empty) is pending. Diff: 2 files, 8
added / 1 removed — flattenTokens early-return guard at
braces.zig:591, current > 0 guard in advance() at braces.zig:310,
regression test at brace.test.ts:53-57 covering default, parse:true, and
tokenize:true modes. Test would crash on main due to unchecked self.tokens.items[0] and usize underflow in self.current - 1. No
TODO/FIXME/HACK in diff, no unrelated changes. CodeRabbit: no actionable
comments.
Co-authored-by: SUZUKI Sosuke <sosuke@bun.com>
5b7fe8 glob: pass pattern component as NtQueryDirectoryFile FileName filter on Windows (#28489)
When iterating a directory during glob walking on Windows, pass the
current
pattern component as a kernel-side FileName filter to NtQueryDirectoryFile.
The filesystem driver evaluates it via FsRtlIsNameInExpression, so
non-matching
entries are never serialized to userspace.
The filter is purely a pre-filter — matchPatternImpl still runs on
every
returned entry to handle case sensitivity and 8.3 short-name aliases. A
filter
is only emitted when the NT match is guaranteed to be a superset of the
glob
match:
skipped for * and ** (no benefit / would hide subdirectories
needed for recursion)
skipped for components containing ? (NT matches one UTF-16 code
unit, glob matches one codepoint — would under-match on surrogate pairs)
skipped for components containing [{\!<>" (not
expressible / NT wildcards)
Also handles STATUS_NO_SUCH_FILE, which NtQueryDirectoryFile returns
on the
first call when a filter matches nothing (previously only STATUS_NO_MORE_FILES
was handled).
Benchmark
Synthetic test: 5,000 files per directory, 5 matching (0.1% match
ratio).
Windows 11, AMD Ryzen AI 9 HX 370.
Pattern
Before (min)
After (min)
Speedup
*.target (1 dir × 5000 files)
1.22 ms
0.51 ms
2.39×
pkg-*/*.target (20 dirs × 5000)
36.95 ms
19.84 ms
1.86×
**/*.target (control — filter skipped)
36.54 ms
36.52 ms
unchanged
Real-world bench/glob/scan.mjs over bench/node_modules: ~10-12%
faster on
non-** patterns, unchanged on ** patterns.
24fa20 Validate mock.module() first argument is a string (#28518)
mock.module() calls toString() on its first argument before checking
its type, then passes the result through the module resolver. When a
non-string value like SharedArrayBuffer is passed, toString()
produces "function SharedArrayBuffer() { [native code] }" which the
resolver tries to auto-install as a package, crashing because the
package manager's logger allocator is uninitialized in this context.
Add an isString() check before the toString() call, matching the
validation pattern used by Jest.call() and other Bun APIs.
e94c30 Fix missing log.deinit() in TOML.parse (#28492)
TOML.parse was missing defer log.deinit() after Log.init, causing
the logger's internal message ArrayList to leak on every call that
produces parse errors. All peer parsers (JSONC, JSON5, YAML) already
have this defer.
Also modernized argument access from callframe.arguments_old(1).slice() to callframe.argument(0) to match
the JSONC parser pattern.
Crash fingerprint: 49da9789c26e29ab
Verification: Confirmed main has Log.init on line 30 with no
matching defer log.deinit(). All peer parsers (JSONCObject.zig:31,
JSON5Object.zig:70, YAMLObject.zig:951) pair Log.init with defer log.deinit(). The log.toJS + defer log.deinit() combination is safe
(same pattern in JSONCObject.zig:31+44). Argument modernization to callframe.argument(0) matches JSONCObject.zig:32 exactly. Regression
test exercises the error path with non-string input and GC. No
TODO/FIXME/HACK in diff. Lint JS and pipeline passed; buildkite build
#41538 compiling (trivial one-line semantic change, adding a missing
defer).
Reviewer verification (robobun): Independently confirmed TOMLObject.zig
on main (line 30) has Log.init with no defer log.deinit(), while
peer JSONCObject.zig (lines 30-31) pairs them correctly. The diff adds
exactly that missing defer and modernizes argument access to match
JSONCObject.zig:32. No TODO/FIXME/HACK in added lines. Bot reviews
(claude x2, coderabbit) all LGTM with no blocking issues. Test exercises
the modified error paths with non-string inputs. CI: Lint JS pass,
pipeline pass, buildkite build #41557 compiling on commit 12daf4b5.
36f17c Fix vm.Script/SourceTextModule/compileFunction leak via fetcher owner cycle (#28493)
What does this PR do?
Fixes a 100% leak of vm.Script, vm.SourceTextModule, and vm.compileFunction results. Every call to these APIs leaked the
resulting object, regardless of user code.
NodeVMScriptFetcher::m_owner was a JSC::Strong handle pointing back
to the owning script/function/module wrapper. Since the owner keeps the
fetcher alive via the SourceCode → SourceProvider → SourceOrigin → RefPtr<fetcher> chain, and the fetcher kept the owner alive as a GC
root, nothing could ever be collected.
Before / After (heapStats objectTypeCounts, 500 iterations + GC)
API
Before
After
new vm.Script("1+1")
Script: +500
+0
vm.compileFunction("return 1")
FunctionExecutable: +500
+0
new vm.SourceTextModule("...")
NodeVMSourceTextModule: +500
+0
The fix
Switch m_owner from JSC::Strong<JSC::Unknown> to JSC::Weak<JSC::JSCell>. The owner is always a JSCell, so Weak is
appropriate. When the owner becomes unreachable it is collected, its m_source chain drops the last RefPtr to the fetcher, and the fetcher
is freed.
owner() is only read during dynamic import (import() inside the
script), at which point the executing context keeps the owner reachable
— so the Weak ref always resolves when it matters. If the owner has
already been collected (e.g. SourceProvider still cached in JSC's
CodeCache but the script cell is gone), owner() returns jsUndefined(), but dynamic import can no longer be triggered at that
point anyway.
This follows the same back-reference-via-Weak pattern as JSCommonJSModule::m_parent
(src/bun.js/bindings/JSCommonJSModule.h:69), JSEventListener::m_wrapper, and JSValueInWrappedObject::m_cell.
Known limitation (separate issue)
m_dynamicImportCallback remains JSC::Strong because it can hold
non-cell values (the USE_MAIN_CONTEXT_DEFAULT_LOADER constant). If a
user callback captures the owner in its closure, a separate cycle forms.
This is user-code-dependent and requires a different approach
(visitChildren from the owner side, complicated by JSFunction being
JSC-internal). Since the callback is passed before the owner is
created, this is uncommon in practice.
How did you verify your code works?
New regression test test/js/node/vm/vm-script-fetcher-leak.test.ts
(4 cases) — fails on main, passes with this fix
Adversarial: Bun.gc(true) × 5 before dynamic import → ref === script still true (Weak resolves correctly while owner is reachable)
594f42 fix crash in expect.extend with numeric index keys (#28504)
expect.extend iterates the matchers object and calls putDirect for
each property. putDirect asserts !parseIndex(propertyName), which
fails when numeric keys like 1073741820 (valid array indices) are
present.
Set own_properties_only and only_non_index_properties to true on
the JSPropertyIterator so index properties are skipped during
enumeration.
However, it does not update `PackageManager.log`. When module
resolution triggers auto-install (e.g. via `mock.module()` with a
non-existent specifier), the package manager calls
`manager.log.addErrorFmt()` on its stale `log` pointer — which may
point to a destroyed stack frame from a previous call to
`resolveMaybeNeedsTrailingSlash`.
This causes a stack-buffer-overflow (ASAN fingerprint:
`Address:stack-buffer-overflow:bun-debug+0xc9f417a`).
Fix
Apply the same save/restore pattern already used in `ModuleLoader.zig`
(lines 188-198) to also update and restore `pm.log` in the defer
block:
Cherry-picks 4 memory safety fixes from upstream WebKit into src/bun.js/bindings/webcore/.
Why
Bun's WebCore code was forked from WebKit ~2022-03 and has since missed
several GC/memory safety fixes. These are minimal, surgical backports of
confirmed bug fixes.
Changes
1. MessagePortChannel: drop messages to closed ports
Upstream: WebKit 281662
Messages sent to a closed MessagePort were queued indefinitely in m_pendingMessages, never to be delivered. Measured: 5000 × 64KB postMessage to closed port: RSS growth 332MB
→ 1.5MB
2. JSAbortController: visit signal.reason in GC
Upstream: WebKit 293319 controller.signal.reason was not marked during GC when only the
controller was retained, causing reason to become undefined after
collection.
3. BroadcastChannel: use ThreadSafeWeakPtr in global map
Upstream: WebKit 267883
The global channel map held raw BroadcastChannel* pointers. If a
worker-owned channel was being destroyed while the main thread looked it
up, the resulting RefPtr assignment could race. ThreadSafeWeakPtr::get() atomically checks liveness via the control
block.
Upstream: WebKit 292397 + 304961
Adds releaseAssertOrSetThreadUID() to mutation operations. Unlike a
global isMainThread() check, this records the owning thread
per-instance, so worker-owned EventTargets work correctly. GC thread
sweeps are exempted.
When createErrorInstance (and related functions) format an error
message, the {f} formatter may call into JS (e.g. via Symbol.toPrimitive). If the formatting itself throws a JS exception
(like "Cannot convert a symbol to a string" when Symbol.toPrimitive
returns a Symbol), the catch branch returns a fallback format string but
leaves the exception pending on the VM.
When throwPretty → throwValue → JSC__VM__throwError then tries to
throw the new error, it hits scope.assertNoException() and crashes
because there's already a pending exception.
Fix: Call clearExceptionExceptTermination() in the catch branch of
all four create*ErrorInstance functions before returning the fallback,
so the VM is in a clean state for the subsequent throwValue.
Verification (commit 362eb28): Lint JS ✅, buildkite #41519 still
building (no individual step has failed; pipeline passed). Diff
confirmed correct — four catch branches in JSGlobalObject.zig gain clearExceptionExceptTermination() calls (7 existing usages in same
file), each returning the correctly-typed error instance
(TypeError/SyntaxError/RangeError). createDOMExceptionInstance
correctly unchanged (uses try propagation). Regression test spawns
subprocess with exact repro, asserts exit 0 only (no forbidden
assertion-absence checks). All 6 coderabbit+claude review comments
addressed across follow-up commits. No TODO/FIXME/HACK in added lines.
JSAbortAlgorithm held its JS callback via JSCallbackDataStrong,
which creates a JSC::Strong handle (a GC root). The abort algorithm
closure registered by pipeTo captures pipeState, which in turn holds pipeState.signal (the JSAbortSignal wrapper). Since the wrapper
holds a Ref<AbortSignal>, the cycle is complete and nothing can be
collected even after all user-side references are dropped.
Before / After
Bun (before)
Bun (after)
Node.js
200 iterations, pipe never completes
201 AbortSignals retained
1
~0
The fix
Switch JSAbortAlgorithm from JSCallbackDataStrong to JSCallbackDataWeak, and keep the callback alive by visiting it from JSAbortSignal::visitAdditionalChildrenInGCThread while the signal
wrapper is reachable.
This follows two existing patterns in the codebase:
JSCallbackDataWeak + visitJSFunction override — same as JSPerformanceObserverCallback
(src/bun.js/bindings/webcore/JSPerformanceObserverCallback.{h,cpp})
Lock + Vector + GC-thread visit — same as EventListenerMap::visitJSEventListeners
(src/bun.js/bindings/webcore/EventListenerMap.h:77-85), which already
protects every addEventListener/removeEventListener against
concurrent GC iteration
Why a separate m_abortAlgorithms vector?
The existing m_algorithms stores type-erased Function<void(JSValue)>
lambdas. Erasing Ref<AbortAlgorithm> into a lambda would hide it from
the GC thread — there would be no way to call visitJSFunction on it.
Storing Ref<AbortAlgorithm> directly in a separate vector lets visitAbortAlgorithms iterate and mark the weak callbacks.
The Locker in runAbortSteps moves the vector out under the lock and
releases before calling handleEvent, so we never hold the lock during
JS re-entry.
How did you verify your code works?
New regression test test/js/web/streams/pipeTo-signal-leak.test.ts
fails on main (201 leaked) and passes with this fix
Existing test/js/web/abort/abort.test.ts, test/js/deno/abort/abort-controller.test.ts, test/js/node/util/test-aborted.test.ts, test/js/web/streams/streams.test.js all pass
Concurrent GC stress test (BUN_JSC_useConcurrentGC=1, 500
controllers × 10 pipes) — no crash, no deadlock
fe4a66 Speed up URLPattern test/exec by calling RegExp::match directly (#28447)
What
Replace RegExpObject::create() + exec() + JSArray readback with
direct RegExp::match() calls using the ovector buffer.
Why
The previous implementation allocated 3 GC objects per component
(RegExpObject, JSString, result JSArray) × 8 components = 24 GC
allocations per test/exec call, then read captures back through JS
property access. Since JSC::RegExp already holds the compiled YARR
state, we can call match() directly and read capture offsets from the
ovector without any JS roundtrip.
Benchmark (Apple M4 Max, release build)
Benchmark
Before
After
Speedup
test() match - named groups
1.05 µs
487 ns
2.16x
test() no-match
579 ns
337 ns
1.72x
test() match - simple
971 ns
426 ns
2.28x
test() match - string pattern
946 ns
434 ns
2.18x
exec() match - named groups
1.97 µs
1.38 µs
1.43x
exec() no-match
583 ns
336 ns
1.73x
exec() match - simple
1.89 µs
1.30 µs
1.45x
bun bench/snippets/urlpattern-detailed.mjs
Changes
URLPatternComponent::componentMatch (new): combines the old
`compone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updated Packages