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
Core: Disable component manifest by default - #34408, thanks @yannbf!
[!NOTE] Version >=0.5.0 of @​storybook/addon-mcp enables component manifests again. If you're upgrading Storybook from version >= 10.3.0 to >= 10.3.5 and are using the MCP addon, you should also upgrade @​storybook/addon-mcp to keep the docs toolset in the MCP server.
Core: Disable component manifest by default - #34408, thanks @yannbf!
[!NOTE] Version >=0.5.0 of @​storybook/addon-mcp enables component manifests again. If you're upgrading Storybook from version >= 10.3.0 to >= 10.3.5 and are using the MCP addon, you should also upgrade @​storybook/addon-mcp to keep the docs toolset in the MCP server.
Core: Disable component manifest by default - #34408, thanks @yannbf!
[!NOTE] Version >=0.5.0 of @​storybook/addon-mcp enables component manifests again. If you're upgrading Storybook from version >= 10.3.0 to >= 10.3.5 and are using the MCP addon, you should also upgrade @​storybook/addon-mcp to keep the docs toolset in the MCP server.
Core: Disable component manifest by default - #34408, thanks @yannbf!
[!NOTE] Version >=0.5.0 of @​storybook/addon-mcp enables component manifests again. If you're upgrading Storybook from version >= 10.3.0 to >= 10.3.5 and are using the MCP addon, you should also upgrade @​storybook/addon-mcp to keep the docs toolset in the MCP server.
191edc image: preserve ICC profile through WebP decode/encode (#30211)
Closes #30197. Follow-up to #30201, which added ICC carry-through for
JPEG and PNG but left WebP dropping the profile because libwebpmux/libwebpdemux weren't linked.
Repro
// any JPEG/PNG with an embedded ICC profile — P3, Adobe RGB, Jpegli XYBawaitBun.file("p3.png").image().webp().write("out.webp");// out.webp had no ICCP chunk → viewers reinterpret as sRGB → colours shift
And the reverse direction: a WebP carrying an ICCP chunk lost it on
decode, so webp → png/jpeg also shifted colour.
Cause
WebP stores ICC profiles in an ICCP chunk inside a VP8X RIFF container
that wraps the VP8/VP8L bitstream. WebPDecodeRGBA/WebPEncodeRGBA
only touch the bitstream chunk; reading or writing sibling chunks needs
the separate demux/mux APIs, and Bun only compiled src/{dec,enc,dsp,utils}.
Fix
Build (scripts/build/deps/libwebp.ts): add src/demux/*.c and src/mux/*.c from the same libwebp checkout. Plain C, no new deps, same
include paths.
Decode (src/image/codec_webp.zig): after WebPDecodeRGBA, run WebPDemux on the original bytes, check WEBP_FF_FORMAT_FLAGS & ICCP_FLAG, and WebPDemuxGetChunk("ICCP") the profile into Decoded.icc_profile (duped into bun.default_allocator to match
JPEG/PNG ownership). A plain VP8/VP8L WebP with no VP8X wrapper falls
through with null.
Encode: webp.encode now takes icc_profile: ?[]const u8. When null/empty, keep the existing one-shot WebPEncodeRGBA fast path
(bare VP8/VP8L, no VP8X). When set, pass the bitstream through WebPMuxSetImage + WebPMuxSetChunk("ICCP") + WebPMuxAssemble to
produce a VP8X-wrapped file and hand the assembled buffer to JS with WebPFree as the finaliser.
codecs.zig / Image.zig / bun.d.ts comments updated to drop the
"WebP loses the profile" caveat.
Verification
New tests in the existing describe("ICC profile") block of test/js/bun/image/image.test.ts walk the output RIFF container to find
the ICCP fourcc and compare the payload byte-for-byte:
PNG iCCP → WebP lossy → ICCP chunk present, VP8X flag bit set
When a specifier contains non-ASCII characters, specifier.toUTF8() in resolveMaybeNeedsTrailingSlash heap-allocates a UTF-8 buffer (because
the underlying WTF string is Latin-1 or UTF-16 and needs converting).
For http://, https://, and // prefixes the resolver marks the
specifier as external and returns a Path.init(import_path) that points
directly into that temporary buffer.
resolveMaybeNeedsTrailingSlash then wrapped that slice in a borrowing bun.String.init(result.path) and freed the buffer via defer specifier_utf8.deinit() before returning. Callers in both Zig
(doResolveWithArgs) and C++ (moduleLoaderResolve, moduleLoaderImportModule) subsequently read poisoned memory when
formatting or converting the result to a JS string.
The query_string out-param had already been fixed to clone in the same
way; result.path needed the same treatment.
How
Clone result.path into an owned bun.String via bun.String.cloneUTF8.
The hardcoded-builtin branch that returned specifier now returns specifier.dupeRef() so all success paths return an owned string.
All callers (doResolveWithArgs, NodeModuleModule.findPath, and the
two C++ Zig__GlobalObject__resolve call sites) now deref() the
successful result after use.
This also fixes a pre-existing leak where onResolveJSC (plugin
onResolve) returned an owned WTFStringImpl that was never deref'd.
b34c77 Shrink Windows binary: lazy-heap threadlocal PathBuffers + /OPT:SAFEICF (#30219)
Windows bun.exe is ~15 MB larger than Linux bun and ~40 MB larger
than macOS. Section-contribution analysis of the canary PDB shows where
it goes:
Section
Windows
Linux
Delta
Cause
.text
60.0 MB
55.8 MB
+4.2 MB
/OPT:NOICF (Linux uses
-icf=safe)
.rdata
36.6 MB
32.4 MB
+4.2 MB
ICU data + no tail merge
.pdata
1.0 MB
—
+1.0 MB
x64 SEH unwind (required)
.tls
4.8 MB
280 B
+4.8 MB
this PR
.reloc
0.2 MB
—
+0.2 MB
ASLR
.tls — 4.8 MB of literal zeros
5,069,287 of 5,069,312 bytes (99.9995%) of the .tls section are 0x00. Of that, 4,998,432 bytes come from bun-zig.o.
Root cause: bun.PathBuffer is [std.fs.max_path_bytes]u8. On Windows
that's 32767*3+1 = 98302 bytes (vs 4096 on POSIX). There are ~50 threadlocal var x: bun.PathBuffer = undefined declarations — resolver.zig alone has 25 of them in the bufs struct (~2.5 MB).
PE/COFF has no TLS-BSS equivalent and lld-link doesn't use IMAGE_TLS_DIRECTORY.SizeOfZeroFill, so every zero-initialized
threadlocal is written into the .tls section as raw zeros in the file and copied into every thread's TLS block at creation whether or not
that thread ever touches the resolver.
Fix: new bun.ThreadlocalBuffers(T) wraps a struct of large buffers
behind a single lazily-heap-allocated per-thread pointer. 8 bytes on
disk per instantiation; backing memory allocated on first get().
Applied to:
resolver.zigbufs() (25 PathBuffers + [2*MAX_PATH_BYTES] win32
buf) — the accessor signature is unchanged so callers don't move
Expected .tls after: ~8 KB (pointers + the few small non-PathBuffer
threadlocals).
Secondary benefit: threads that never hit the resolver/installer (e.g. Workers running pure compute) no longer pay ~5 MB of TLS-block copy at
spawn.
Linker flags (Windows release)
/OPT:NOICF → /OPT:SAFEICF. The previous attempt (commit
d7c6d59f02) used aggressive /OPT:ICF, which folded callBigIntConstructor with constructWithBigIntConstructor
(byte-identical bodies that both throw) → JSC's InternalFunction
pointer-identity check broke → "BigInt is not a constructor" and expect.any(Ctor) failures → reverted in 218430c731. /OPT:SAFEICF
(lld-specific) skips address-taken functions, which is exactly what
those ClassInfo function pointers are, so the identity checks survive.
This is the same guarantee Linux already gets from -Wl,-icf=safe.
/OPT:lldtailmerge — lld-specific string-literal tail merging; no
MSVC link.exe equivalent.
/FILEALIGN:0x200 — was in the old CMake config (kept alongside
the /OPT:NOICF revert), lost in the ninja migration.
What this does NOT touch
Debug symbols: PDB generation unchanged (/DEBUG:FULL still set;
PDB is a separate file).
napi / libuv: src/symbols.def unchanged; no exports removed.
ICU data (24.6 MB of .rdata): also present on Linux; macOS uses
system libicucore. Windows icu.dll isn't ABI-compatible with what
WebKit needs without upstream changes, so it stays for now. The existing icupkg -r filter already removes ~6.8 MB of converters/translit/rbnf.
.pdata (1.0 MB): Windows x64 SEH unwind tables are required for
structured exception handling and can't be stripped.
test/js/bun/jsc/native-constructor-identity.test.ts added as a
trip-wire for the ICF constructor-identity regression (BigInt/Symbol not
constructable, expect.any across Map/Set/WeakMap/WeakSet, all 11
typed-array constructors distinct, Request/Response/Blob distinct)
Expected Windows x64 reduction: ~5 MB from .tls alone; SAFEICF +
tailmerge + FILEALIGN should recover another ~2–4 MB from .text/.rdata. Actual numbers from Windows CI artifacts.
bab007 socket: set Handlers.mode=.client for Windows named-pipe Bun.connect (#30150)
Repro
Windows only:
awaitBun.connect({unix: '\\\\.\\pipe\\x',socket: {data(){},open(){},close(){}},});// then close (or fail) the connection
On close, Handlers.markInactive() hits active_connections == 0 with .mode == .server and does @​fieldParentPtr("handlers", this)
expecting an enclosing Listener — but the handlers live in a
standalone allocator.create(Handlers) block, so reading listen_socket.listener falls past the allocation. Under ASAN that's a
heap-buffer-overflow; on release it reads garbage and — because the .client branch is skipped — leaks the block.
Cause
connectInner() calls SocketConfig.fromJS(vm, opts, globalObject, true) at Listener.zig:564. The last argument is is_server, which
feeds handlers.mode. It was false until 4a06991d3b (#23755) flipped
it during a bindings-generator refactor.
The non-pipe path at :797 has always had an explicit handlers_ptr.mode = .client after copying into the heap block (it was handlers_ptr.is_server = false before #26539), which masked the flip
everywhere except the Windows named-pipe early-return at :655–656, which
never had one.
is_server is only used to set handlers.mode; nothing else in SocketConfig.fromGenerated / Handlers.fromGenerated branches on it.
Fix
Restore is_server=false at the connectInner call site (this is the
client connect path).
Add the same defensive handlers_ptr.mode = .client on the named-pipe
branch to mirror the non-pipe branch, so the two copies into a
standalone Handlers block look the same.
Audited the other standalone allocator.create(Handlers) sites:
socket.zig:1557 — sourced from Handlers.fromJS(..., false),
already .client.
socket.zig:2062 — explicit .mode = if (is_server) .duplex_server else .client.
Verification
bun run zig:check-all passes (all targets, including both Windows
arches).
New Windows-only tests in test/js/bun/net/socket.test.ts:
Listen on a named pipe, Bun.connect to it, close → clean exit.
Bun.connect to a non-existent pipe → rejects, clean exit.
Both are spawned in a subprocess so an ASAN crash surfaces as a non-zero
exit instead of killing the test runner. Skipped on non-Windows (the if (Environment.isWindows) branch is unreachable there, and the non-pipe
path's :797 override already covers it).
4f13b9 bun -p: return module completion value, not first yielded await (#30208)
Repro
$ bun -p '(await 1) + 1'1
$ bun -p 'await Promise.resolve("hello") + " world"'hello
Expected: 2 and hello world.
Cause
--print uses ESM module evaluation and captures the last expression
value via EvalGlobalObject::moduleLoaderEvaluate in src/bun.js/bindings/ZigGlobalObject.cpp. For a module with top-level await, JSC generator-ifies the body; the first call into moduleLoader->evaluate() yields the awaited value (1), not the
module's final completion value (2). That yielded value was stored as
the eval result.
The async resume path (asyncModuleExecutionResume in vendor/WebKit/.../JSMicrotask.cpp) calls module->evaluate()
directly and bypasses the moduleLoaderEvaluate hook, so the hook
could never observe the final value and correct itself.
Fix
After the initial evaluateNonVirtual call, inspect the module
record's generator state. If it yielded (state is a number other than Executing), the module still has work left and result is the
awaited value. Store the module's asyncCapability() promise instead
— its eventual resolution is the module's actual completion value.
The bun -p loop in src/bun.js.zig already unwraps promises via asAnyPromise + Bun__onResolveEntryPointResult, so no Zig-side
changes are needed. For non-TLA modules, behavior is unchanged (state
is Executing, result stored as before).
Verification
USE_SYSTEM_BUN=1 bun test test/cli/run/run-eval.test.ts -t 'bun -p'
→ 3 fail, 1 pass
bun bd test test/cli/run/run-eval.test.ts -t 'bun -p' → 4 pass
Full test/cli/run/run-eval.test.ts (33 tests) and TLA regression
tests still pass.
Fixes #30207
Co-authored-by: robobun <robobun@bun.sh>
6acb78 Make it easy to compare canary vs previous release build size
31c494 socket: balance ref on synchronous doConnect failure for reused sockets (#30168)
Listener.connectInner unconditionally socket.ref()s before calling doConnect, for both freshly-allocated sockets and reused ones passed
as prev (the node:net path — _handle is a detached native socket
from newDetachedSocket).
When doConnect fails synchronously (ENOENT unix path, bad fd, EMFILE),
the socket never leaves .detached, so handleConnectError's needs_deref = !this.socket.isDetached() is false and its own deref
does not fire. The caller is responsible for balancing the ref — but the
existing line only did so when maybe_previous == null:
if (maybe_previous==null) socket.deref();
That guard was added in #23936 to fix the Bun.connect({fd: badFd})
leak (fresh-socket case) but left the reused-socket case unbalanced: one
native TCPSocket struct + its connection string leak per failed
reconnect.
Fix
Drop the guard. The ref() at :849 is unconditional, so the balancing deref() on sync failure must be too.
Verification
New test in test/js/node/net/node-net.test.ts does 20k failed unix
connects in a subprocess and samples RSS after equal-sized work units. A
real leak grows linearly; noise plateaus.
RSS growth over 12.5k post-warmup iterations
before (debug+ASAN)
~14 MB
before (release)
~6 MB
after (debug+ASAN)
±1 MB
Threshold 3 MB. The original #23936 test (Bun.connect with bad fd) and socket-retention.test.ts still pass.
a47ccf socket: null handlers pointer after client-mode Handlers are freed (#30176)
What
Follow-up to #30148, which nulls this.handlers in the socket's markInactive() after Handlers.markInactive() frees the client-mode
allocation. That covers the onClose → This.markInactive() path, but
not the paths where scope.exit() is the decrement that frees the
handlers — most notably handleConnectError, where the socket never
reaches markActive() so is_active == false and the deferred this.markInactive() is a no-op.
Handlers.markInactive() (client mode, active_connections → 0) does this.deinit(); vm.allocator.destroy(this). Any caller that still holds
the pointer — the socket's handlers field — must clear it, otherwise:
Listener.connectInner at :664 / :728 / :814 — a reconnect through
the same native socket as prev calls prev_handlers.deinit() then allocator.destroy(prev_handlers) on freed memory (UAF + double-free).
socket.ziggetListener — reads handlers.mode on freed memory.
Repro
constnet=require('node:net');consts=newnet.Socket();lethandle;s.on('error',()=>{});s.once('connectionAttemptFailed',()=>{handle=s._handle;});s.on('close',()=>{// handleConnectError's scope.exit() freed the Handlers; the// socket-level markInactive() never ran (is_active == false).handle.listener;// ← UAF on current main});s.connect(1,'127.0.0.1');
Under debug+ASAN on current main (after #30148):
AddressSanitizer: use-after-poison
READ of size 1 ...
#0 NewSocket(false).getListener src/bun.js/api/bun/socket.zig:769
Fix
Handlers.markInactive() and Scope.exit() now return whether the
allocation was destroyed. This.markInactive() and every scope.exit()
site in socket.zig null this.handlers when it was. This replaces
#30148's mode-based check with the precise destroyed signal and extends
it to the handleConnectError / handleError paths.
Verification
Three tests in test/js/node/net/node-net.test.ts (gated to
debug/ASAN):
main (incl. #30148)
with this PR
handle.listener after connectError
ASAN use-after-poison @
getListener
undefined
handle.listener after close
passes (covered by #30148)
passes
reconnect via saved native handle
passes (covered by #30148)
passes
The first test is the one that demonstrates this PR's incremental fix.
797dee crypto: fix f32 precision loss and unit mismatch in randomFill bounds checks (#30134)
What does this PR do?
Fixes two bounds-checking bugs in crypto.randomFill / crypto.randomFillSync:
Heap overflow via f32 precision loss in the size + offset > length check
Unit mismatch in the 3-arg randomFill(buf, offset, cb)
default-size computation causing integer underflow or silent under-fill
for multi-byte typed arrays
Reproduction
// (1) writes 1 byte past the end of the allocation instead of throwingrequire('crypto').randomFillSync(newArrayBuffer(16777218),16777217,2);// (2a) panics in debug / throws spurious ERR_OUT_OF_RANGE in releaserequire('crypto').randomFill(newFloat64Array(10),2,()=>{});// (2b) leaves bytes 744..800 un-randomizedrequire('crypto').randomFill(newFloat64Array(100),1,()=>{});
Node.js throws ERR_OUT_OF_RANGE for (1) and fills the full tail for
(2).
Root cause
(1) In assertSize, the u32offset was cast to f32 before
being added to the f64size:
if (size+@​as(f32, @​floatFromInt(offset)) >@​as(f64, @​floatFromInt(length))) {
f32 only represents integers exactly up to 2²⁴ = 16777216. An offset
of 16777217 rounds down to 16777216, so with length = 16777218 and size = 2 the check evaluates 2 + 16777216 > 16777218 → false, when
the true sum 16777219 exceeds length. The bogus offset/size are then
used to slice the buffer (sync) or handed to the threadpool as a raw [*]u8 span (async), producing an OOB write.
(2) In randomFill's 3-arg branch, the default size was computed as buf.len - offset where buf.len is the element count but offset
had already been scaled to a byte offset by assertOffset. For Float64Array(10) with offset 2, that's 10 - 16 → usize underflow.
Fix
Change all four f32 casts in assertOffset / assertSize to f64
(exact for all integers up to 2⁵³, well beyond max_possible_length).
In the 3-arg branch, set size_value = .js_undefined to fall through
to the existing buf.byte_len - offset default, keeping both operands
in byte units.
Verification
bun bd test test/js/node/crypto/crypto-random.test.ts — 14 pass, 0
fail
6d73f5 fs: deref Dirent.path in readdirSync recursive error cleanup (#30167)
What
When fs.readdirSync(dir, { recursive: true, withFileTypes: true })
fails partway through (e.g. a subdirectory returns ELOOP/EACCES on
open), the error-path cleanup in readdirInner was only calling result.name.deref() on each collected Dirent, leaking the ref on Dirent.path that was taken via dirent_path_prev.ref() in readdirWithEntriesRecursiveSync.
The async recursive path (AsyncReaddirRecursiveTask.performWork) and
the non-recursive path (readdirWithEntries) already call Dirent.deref() which releases both name and path. This brings the
sync-recursive error path in line.
Repro
constfs=require('fs');// dir contains a self-referential symlink at depth 2, so the BFS walker// collects a bunch of Dirents before hitting ELOOP and unwinding.for(leti=0;i<30000;i++){try{fs.readdirSync(dir,{recursive: true,withFileTypes: true});}catch{}}// RSS grows linearly with iteration count
Verification
The new test builds a wide tree under a long path with a symlink loop at
depth 2, warms up to saturate ASAN quarantine, then runs 20k failing readdirSync calls and asserts RSS growth stays under 64 MB.
d971e4 webcore/Blob: free allocations on truncated structured-clone deserialize (#30152)
Problem
_onStructuredCloneDeserialize in src/bun.js/webcore/Blob.zig reads a
Blob/File from untrusted bytes — reachable via require('bun:jsc').deserialize, require('node:v8').deserialize, and
cross-process IPC advanced serialization. It allocates at several points
along the way:
readSlice allocates a buffer, reads into it, and returns error.TooSmall on a short read — without freeing the buffer.
content_type is allocated with no errdefer; every subsequent try leaks it.
The bytes payload is allocated and wrapped in a stack Blob (owning
a Store) with no errdefer; the following stored_name
length/payload reads leak the whole thing on truncation.
The stack Blob is heap-promoted via Blob.new; the trailer reads
(is_jsdom_file, last_modified, v3 File name) then leak the heap *Blob, its Store, and its bytes.
The stored_name slice is leaked when the store is null
(zero-length bytes payload).
This is distinct from #30072, which fixed the out-of-bounds offset
clamp in the same function; this is the error-path cleanup.
readSlice: errdefer allocator.free(slice) so a short read releases
the buffer.
After content_type allocation: errdefer allocator.free(content_type) — it isn't attached to the blob until the
very end of the success path.
Inside the .bytes arm: errdefer blob.deinit() on the stack blob so
the Store (and its bytes) are released when the stored_name reads
fail; free name explicitly when there is no store to own it.
After the switch: errdefer blob.deinit() on the heap *Blob so
the trailer reads release the heap object, its Store, and its bytes.
Verification
Two new tests in test/js/web/structured-clone-blob-file.test.ts:
truncated payload at every byte boundary throws cleanly —
serializes a File, slices it at every byte offset, and asserts each deserialize throws rather than crashing or returning a half-built
Blob. Sweeps every error edge in the decoder.
truncated payload does not leak ... — serializes a File with
64 KiB of content-type and 64 KiB of body, truncates at five points
chosen to land after each allocation site, and loops deserialize on
them. Measures RSS across 1500 iterations after a warmup.
d6f215 fix(Bun.serve): HEAD response Transfer-Encoding/Content-Length freed before write (#30155)
Repro
Bun.serve({port: 0,fetch: ()=>newResponse("hello",{headers: [["Transfer-Encoding","gzip"],["Transfer-Encoding","chunked"],],}),});// HEAD / → ASAN heap-use-after-free in uWS::HttpResponse::writeHeader
The duplicate entries make FetchHeaders combine them via makeString(), producing a StringImpl held only by the header map —
the minimal condition for the free to actually happen.
StringImpl is allocated via bmalloc which ASAN doesn't instrument by
default; with Malloc=1 (bmalloc → system heap) the debug build
reports:
doRenderHeadResponse() calls headers.fastGet(.TransferEncoding),
which returns a ZigString that borrows the header map entry's StringImpl bytes (no ref taken). For an ASCII value, toSlice() also
borrows rather than copying. It then calls this.renderMetadata(),
whose doWriteHeaders() does headers.fastRemove(.TransferEncoding)
(and renderMetadata also swapInitHeaders() + deref()s the whole FetchHeaders). When the map held the only reference to the StringImpl, it's destroyed right there — and the very next line resp.writeHeader("transfer-encoding", transfer_encoding_str.slice())
writes the freed bytes to the socket.
The adjacent Content-Length branch has the same bug: std.fmt.parseInt() runs on the borrowed slice after renderMetadata() has already fastRemove(.ContentLength)'d it.
Fix
Transfer-Encoding: use toSliceClone() instead of toSlice() so
the value is owned and survives renderMetadata().
Content-Length: parse the integer beforerenderMetadata() (and
drop the slice immediately), so the borrowed bytes are never touched
after the header entry is removed. No extra allocation needed since only
the parsed usize is used afterwards.
Verification
New test in test/js/bun/http/bun-server.test.ts (inside the existing HEAD requests #15355 block) spawns a subprocess with Malloc=1
(non-Windows), serves HEAD responses whose Transfer-Encoding /
Content-Length values are makeString()-combined (sole-owner
StringImpl), and asserts the raw wire output.
git stash push -- src/ → test fails with "AddressSanitizer: heap-use-after-free" in stderr
git stash pop → test passes
All other tests in the HEAD requests #15355 describe block continue to
pass.
096a24 sql(mysql): pin ArrayBuffer backing store while binding BLOB parameters (#30159)
Repro
constbuf=newArrayBuffer(64);constta=newUint8Array(buf);for(leti=0;i<ta.length;i++)ta[i]=i;constvalues=[1,ta,"placeholder"];letcalls=0;Object.defineProperty(values,"2",{get(){if(++calls>=2&&buf.byteLength>0){// zero-copy transfer: same backing pointernewUint8Array(buf.transfer()).fill(0xff);}return"evil";},});awaitsql.unsafe(`INSERT INTO t (id, data, name) VALUES (?, ?, ?)`,values);// stored `data`: 64 × 0xff — should be 0x00..0x3f
Cause
Value.fromJS for MYSQL_TYPE_*BLOB returned ZigString.Slice.fromUTF8NeverFree(array_buffer.slice()), borrowing the
backing store without protecting it.
MySQLQuery.bind() collects every parameter into a []Value first and
only then calls execute.write(). Converting later parameters can run
user JS — array index getters via QueryBindingIterator.next(), toJSON via jsonStringifyFast, toString via bun.String.fromJS —
and that JS can transfer()/detach an earlier buffer, or drop the last
JS reference to it and force GC. execute.write() then reads bytes the
caller no longer owns.
For a non-resizable ArrayBuffer, buf.transfer() with no arguments is
zero-copy in JSC: the new buffer takes ownership of the same backing
pointer, so overwriting the new buffer mutates exactly what the borrowed
slice still points at. With a resizing transferToFixedLength(n) the
old backing store is freed outright.
(The Postgres path doesn't have this window: PostgresRequest.writeBind
writes each parameter to the wire inside the loop before touching the
next one.)
Fix
bindAndExecute now runs inside a stack-scoped MarkedArgumentBuffer
(same pattern as udp_socket.zigsendMany) that Value.fromJS
appends borrowed buffer/Blob wrappers to, and the backing ArrayBuffer
is pinned via JSC__JSValue__borrowBytesForOffThread (the same helper Bun.Image uses):
Oversize/Wasteful/DataView/JSArrayBuffer → ArrayBuffer::pin()
makes it non-detachable — transfer() hands the user a copy and
leaves the original backing store intact. The wrapper is appended to the MarkedArgumentBuffer so GC can't sweep the cell whose RefPtr<ArrayBuffer> keeps the storage alive (params lives on the
malloc heap and isn't scanned).
FastTypedArray (≤ ~1 KB, GC-movable vector) → bytes are duped.
Pinning would force slowDownAndWasteMemory() which copies anyway.
Blob → plain borrow (immutable store, no detach); wrapper appended
to the MarkedArgumentBuffer so the store survives GC.
Value.bytes now carries the JSValue to unpin alongside the slice:
Value.deinit() — already run via Execute.deinit() after execute.write(), inside the MarkedArgumentBuffer scope — calls JSC__JSValue__unpinArrayBuffer(pinned) and frees the dupe via slice.deinit().
Verification
test/js/sql/sql-mysql-bind-blob-borrow.test.ts primes the
prepared-statement cache so the second call goes straight to bindAndExecute, then binds [id, Uint8Array(buf), <getter>] where the
getter transfer()s buf and fills the result with 0xff during the
bind loop. It also asserts buf is detachable again after the query
resolves (pin released).
Fail-before (src/ reverted, test kept):
{
"detachableAfter": true,
"detached": true,
- "gotHex": "000102…3f",
- "match": true,
+ "gotHex": "ffffff…ff",
+ "match": false,
"originalHex": "000102…3f",
}
(fail) mysql (local) > BLOB param backing store is pinned across the bind loop
a50b47 fix(ipc): run SendQueue.deinit() from IPCInstance.deinit on getIPCInstance failure (#30177)
What
Follow-up to #30051, which added SendQueue.after_close_task tracking
so SendQueue.deinit() can cancel a pending _onAfterIPCClosed task
before the owner frees it.
IPCInstance.deinit was still TrivialDeinit → bun.destroy, so on
the getIPCInstance error path the embedded SendQueue was never
deinited and the tracked task was never cancelled.
On Windows, windowsConfigureClient sets data.socket = .open before calling uv_read_start. If uv_read_start fails, it calls closeSocket() which queues the _onAfterIPCClosed task (socket was .open), returns an error, and getIPCInstance then calls instance.deinit() — freeing the IPCInstance and its embedded SendQueue with the task still queued.
Fix
Replace TrivialDeinit with an explicit deinit that runs this.data.deinit() before bun.destroy(this), so the after_close_task cancel path added in #30051 actually fires for this
owner too.
Test
Added a case in spawn.ipc.test.ts that drives a child through the getIPCInstance error path with an unusable NODE_CHANNEL_FD and
verifies clean teardown. The specific uv_read_start-fails-after-uv_pipe_open-succeeds trigger is
Windows-only and not deterministically reproducible from userland; the
test covers the surrounding error-path teardown on both platforms.
570653 server: hold Response via WeakPtr instead of a raw pointer (#30174)
What
RequestContext stored response_ptr: ?*Response and, for plain Blob/InternalBlob/WTFStringImpl bodies, left the Response JSValue
unprotected. renderBytes() → tryEnd() can hit backpressure and
register an onWritable callback, unwinding with response_ptr still
set. Nothing rooted the Response (RequestContext is a pool struct, not
GC-visited), so GC could finalize it. If the client then aborted while
the request body was still .Locked, onAbort() dereferenced a freed *Response — heap-use-after-free under ASAN at RequestContext.zig:692.
Give Response a weak_ptr_data field (mirroring Request.WeakRef)
and replace response_ptr: ?*Response with response_weakref: Response.WeakRef via bun.ptr.WeakPtr. Response.destroy() now defers
freeing the allocation until outstanding weak refs drop; WeakRef.get()
returns null once the contents are gone.
onAbort / handleResolveStream / handleRejectStream call .get()
and simply skip the readable-stream cleanup when it's null — a no-op for
in-memory bodies anyway, since the body was already extracted via useAsAnyBlobAllowNonUTF8String() before backpressure.
File-backed and .Locked bodies continue to protect() response_jsvalue as before; those paths need the Response's
status/headers alive across the async hop for renderMetadata(). The
hot path (small in-memory responses) no longer needs protect()/unprotect().
The two redundant ctx.response_ptr = response assignments right before ctx.render(response) are dropped — render() already sets the weak
ref.
Verification
test/js/bun/http/serve-response-gc-backpressure-abort.test.ts
(ASAN/debug-only): POST with incomplete chunked body so request_body
stays .Locked, handler returns a large string Response, client pauses
so tryEnd() stalls, Bun.gc(true) loop, then client closes.
without fix: AddressSanitizer: use-after-poison in onAbort → Response.getBodyReadableStream
with fix: passes, abortCount === iterations, pendingRequests === 0
JSC::Strong<T> has no move constructor. Capturing it by value
copy-constructs it, which calls HandleSet::allocate() + m_strongList.push(); destroying it calls HandleSet::deallocate() + NodeList::remove(). Both happen on the worker thread against the parent VM'sHandleSet, without the parent VM's lock.
HandleSet::m_strongList is a SentinelLinkedList<HandleNode> — not
thread-safe. push/remove transiently null m_next/m_prev. The
parent VM's "Sh" (Strong Handles) marking constraint
(Heap::addCoreConstraints) iterates that list during GC; when it
follows a null m_next it reads *((HandleNode*)nullptr)->slot() → *(0x0 + 0x10).
The heapHelperPool() is process-global, so the crashing helper thread
belongs to the parent VM's collector even though the worker VM's BunV8HeapSnapshotBuilder full GC is in progress at the same time.
This has been there since getHeapSnapshot was added — the recent
worker lifetime rewrites (#29957, #29937) didn't introduce it.
Fix
Heap-allocate the Strong<JSPromise> once on the parent thread and pass
only the raw pointer through the cross-thread lambdas. The worker thread
never dereferences it, so it never touches the parent VM's HandleSet.
The parent-side completion lambda resolves the promise and frees the
handle.
Worker::postTaskToWorkerGlobalScope now returns bool so a lost race
to Closing/Closed (worker exited between isOnline() and the post)
rejects with ERR_WORKER_NOT_RUNNING instead of silently leaking the
handle. If postTaskTo(parentId, …) on the return trip fails (parent
context gone), the handle intentionally leaks — deleting a parent-VM Strong from the worker thread is exactly the bug we're fixing, and the
parent VM is tearing down anyway.
Verification
Stress fixture (heap-snapshot-gc-race-fixture.js, 300 iterations of await worker.getHeapSnapshot(); Bun.gc(true)), 40 runs each on
linux-x64 release:
build
segfault at 0x10
52bdf47 (CI artifact, no fix)
15 / 40
this branch
0 / 40
The new worker_heap_snapshot_gc.test.ts runs the fixture — 300 iters
in release, 5 in debug (a single debug heap snapshot takes ~1.6s so the
race window, which is a handful of instructions after each snapshot, is
impractical to hit there; the debug pass is a functional check).
Core: Disable component manifest by default - #34408, thanks @yannbf!
[!NOTE] Version >=0.5.0 of @​storybook/addon-mcp enables component manifests again. If you're upgrading Storybook from version >= 10.3.0 to >= 10.3.5 and are using the MCP addon, you should also upgrade @​storybook/addon-mcp to keep the docs toolset in the MCP server.
Core: Disable component manifest by default - #34408, thanks @yannbf!
[!NOTE] Version >=0.5.0 of @​storybook/addon-mcp enables component manifests again. If you're upgrading Storybook from version >= 10.3.0 to >= 10.3.5 and are using the MCP addon, you should also upgrade @​storybook/addon-mcp to keep the docs toolset in the MCP server.
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