[pull] main from oven-sh:main - #12
Merged
Merged
Conversation
Co-authored-by: cirospaciari <6379399+cirospaciari@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
pull Bot
pushed a commit
that referenced
this pull request
Jul 25, 2025
…ck traces upon crash in CI (oven-sh#21143) ### What does this PR do? Closes oven-sh#13012 On Linux, when any Bun process spawned by `runner.node.mjs` crashes, we run GDB in batch mode to print a backtrace from the core file. And on all platforms, we run a mini `bun.report` server which collects crashes reported by any Bun process executed during the tests, and after each test `runner.node.mjs` fetches and prints any new crashes from the server. <details> <summary>example 1</summary> ``` #0 crash_handler.crash () at crash_handler.zig:1513 #1 0x0000000002cf4020 in crash_handler.crashHandler (reason=..., error_return_trace=0x0, begin_addr=...) at crash_handler.zig:479 #2 0x0000000002cefe25 in crash_handler.handleSegfaultPosix (sig=<optimized out>, info=<optimized out>) at crash_handler.zig:800 #3 0x00000000045a1124 in WTF::jscSignalHandler (sig=11, info=0x7ffe044e30b0, ucontext=0x0) at vendor/WebKit/Source/WTF/wtf/threads/Signals.cpp:548 #4 <signal handler called> #5 JSC::JSCell::type (this=0x0) at vendor/WebKit/Source/JavaScriptCore/runtime/JSCellInlines.h:137 #6 JSC::JSObject::getOwnNonIndexPropertySlot (this=0x150bc914fe18, vm=..., structure=0x150a0102de50, propertyName=..., slot=...) at vendor/WebKit/Source/JavaScriptCore/runtime/JSObject.h:1348 #7 JSC::JSObject::getPropertySlot<false> (this=0x150bc914fe18, globalObject=0x150b864e0088, propertyName=..., slot=...) at vendor/WebKit/Source/JavaScriptCore/runtime/JSObject.h:1433 #8 JSC::JSValue::getPropertySlot (this=0x7ffe044e4880, globalObject=0x150b864e0088, propertyName=..., slot=...) at vendor/WebKit/Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1108 #9 JSC::JSValue::get (this=0x7ffe044e4880, globalObject=0x150b864e0088, propertyName=..., slot=...) at vendor/WebKit/Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1065 #10 JSC::LLInt::performLLIntGetByID (bytecodeIndex=..., codeBlock=0x150b861e7740, globalObject=0x150b864e0088, baseValue=..., ident=..., metadata=...) at vendor/WebKit/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp:878 #11 0x0000000004d7b055 in llint_slow_path_get_by_id (callFrame=0x7ffe044e4ab0, pc=0x150bc92ea0e7) at vendor/WebKit/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp:946 #12 0x0000000003dd6042 in llint_op_get_by_id () #13 0x0000000000000000 in ?? () ``` </details> <details> <summary>example 2</summary> ``` #0 crash_handler.crash () at crash_handler.zig:1513 #1 0x0000000002c5db80 in crash_handler.crashHandler (reason=..., error_return_trace=0x0, begin_addr=...) at crash_handler.zig:479 #2 0x0000000002c59f60 in crash_handler.handleSegfaultPosix (sig=<optimized out>, info=<optimized out>) at crash_handler.zig:800 #3 0x00000000042ecc88 in WTF::jscSignalHandler (sig=11, info=0xfffff60141b0, ucontext=0xfffff6014230) at vendor/WebKit/Source/WTF/wtf/threads/Signals.cpp:548 #4 <signal handler called> #5 bun.js.api.FFIObject.Reader.u8 (globalObject=0x4000554e0088) at /var/lib/buildkite-agent/builds/ip-172-31-75-92/bun/bun/src/bun.js/api/FFIObject.zig:65 #6 bun.js.jsc.host_fn.toJSHostCall__anon_1711576 (globalThis=0x4000554e0088, args=...) at /var/lib/buildkite-agent/builds/ip-172-31-75-92/bun/bun/src/bun.js/jsc/host_fn.zig:97 #7 bun.js.jsc.host_fn.DOMCall("Reader"[0..6],bun.js.api.FFIObject.Reader,"u8"[0..2],.{ .reads = .{ ... }, .writes = .{ ... } }).slowpath (globalObject=0x4000554e0088, thisValue=70370172175040, arguments_ptr=0xfffff6015460, arguments_len=1) at /var/lib/buildkite-agent/builds/ip-172-31-75-92/bun/bun/src/bun.js/jsc/host_fn.zig:490 #8 0x000040003419003c in ?? () #9 0x0000400055173440 in ?? () ``` </details> I used GDB instead of LLDB (as the branch name suggests) because it seems to produce more useful stack traces with musl libc. - [x] on linux, use gdb to print from core dump of main bun process crashed - [x] on linux, use gdb to print from all new core dumps (so including bun subprocesses spawned by the test that crashed) - [x] on all platforms, use a mini bun.report server to print a self-reported trace (depends on oven-sh/bun.report#15; for now our package.json points to a commit on the branch of that repo) - [x] fix trying to fetch stack traces too early on windows - [x] use output groups so the traces show up alongside the log for the specific test instead of having to find it in the logs from the entire run - [x] get oven-sh/bun.report#15 merged, and point to a bun.report commit on the main branch instead of the PR branch in package.json ### How did you verify your code works? Manually, and in CI with a crashing test. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
pull Bot
pushed a commit
that referenced
this pull request
Aug 8, 2025
<details> <summary> observed in https://buildkite.com/bun/bun/builds/22442#annotation-test/js/node/zlib/leak.test.ts </summary> ``` ==5045==ERROR: AddressSanitizer: heap-use-after-free on address 0x5220000243c0 at pc 0x00000dad671b bp 0x14f22d4a4990 sp 0x14f22d4a4988 READ of size 8 at 0x5220000243c0 thread T5 (HeapHelper) ======== Stack trace from GDB for HeapHelper-5045.core: ======== Program terminated with signal SIGABRT, Aborted. #0 0x000014f2c3672eec in ?? () from /lib/x86_64-linux-gnu/libc.so.6 [Current thread is 1 (Thread 0x14f22d4f46c0 (LWP 5050))] #0 0x000014f2c3672eec in ?? () from /lib/x86_64-linux-gnu/libc.so.6 #1 0x000014f2c3623fb2 in raise () from /lib/x86_64-linux-gnu/libc.so.6 #2 0x000014f2c360e472 in abort () from /lib/x86_64-linux-gnu/libc.so.6 #3 0x000000000e3b2ae2 in uw_init_context_1[cold] () #4 0x000000000e3b29fc in _Unwind_Backtrace () #5 0x00000000046a6bab in __sanitizer::BufferedStackTrace::UnwindSlow(unsigned long, unsigned int) () #6 0x00000000046a181d in __sanitizer::BufferedStackTrace::Unwind(unsigned int, unsigned long, unsigned long, void*, unsigned long, unsigned long, bool) () #7 0x00000000046885bd in __sanitizer::BufferedStackTrace::UnwindImpl(unsigned long, unsigned long, void*, bool, unsigned int) () #8 0x0000000004601127 in __asan::ErrorGeneric::Print() () #9 0x0000000004683180 in __asan::ScopedInErrorReport::~ScopedInErrorReport() () #10 0x0000000004686567 in __asan::ReportGenericError(unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long, unsigned int, bool) () #11 0x0000000004686d46 in __asan_report_load8 () #12 0x000000000dad671b in ZSTD_sizeof_CCtx (cctx=<optimized out>) at ./build/release-asan/zstd/vendor/zstd/lib/compress/zstd_compress.c:210 #13 0x0000000006d2284d in bun.js.node.zlib.NativeZstd.estimatedSize () at /var/lib/buildkite-agent/builds/ip-172-31-72-121/bun/bun/src/bun.js/node/zlib/NativeZstd.zig:57 #14 ZigGeneratedClasses.JSNativeZstd.JavaScriptCoreBindings.NativeZstd__estimatedSize (thisValue=<optimized out>) at /var/lib/buildkite-agent/builds/ip-172-31-72-121/bun/bun/build/release-asan/codegen/ZigGeneratedClasses.zig:11122 #15 0x000000000852803b in WebCore::JSNativeZstd::visitChildrenImpl<JSC::SlotVisitor> (cell=0x14f22e190840, visitor=...) at ./build/release-asan/./build/release-asan/codegen/ZigGeneratedClasses.cpp:30728 #16 WebCore::JSNativeZstd::visitChildren (cell=0x14f22e190840, visitor=...) at ./build/release-asan/./build/release-asan/codegen/ZigGeneratedClasses.cpp:30734 #17 0x000000000aa99d6c in JSC::MethodTable::visitChildren (this=<optimized out>, cell=<optimized out>, visitor=...) at vendor/WebKit/Source/JavaScriptCore/runtime/ClassInfo.h:115 #18 0x000000000aa99d6c in JSC::SlotVisitor::visitChildren (this=0x14f277028300, cell=0x14f22e190840) #19 JSC::SlotVisitor::drain(WTF::MonotonicTime)::$_0::operator()(JSC::MarkStackArray&) const (this=<optimized out>, stack=...) at vendor/WebKit/Source/JavaScriptCore/heap/SlotVisitor.cpp:509 #20 0x000000000aa8f130 in JSC::SlotVisitor::forEachMarkStack<JSC::SlotVisitor::drain(WTF::MonotonicTime)::$_0>(JSC::SlotVisitor::drain(WTF::MonotonicTime)::$_0 const&) (this=0x14f277028300, func=...) at vendor/WebKit/Source/JavaScriptCore/heap/SlotVisitorInlines.h:193 #21 JSC::SlotVisitor::drain (this=this@entry=0x14f277028300, timeout=<error reading variable: That operation is not available on integers of more than 8 bytes.>, timeout@entry=...) at vendor/WebKit/Source/JavaScriptCore/heap/SlotVisitor.cpp:499 #22 0x000000000aa90590 in JSC::SlotVisitor::drainFromShared (this=0x14f277028300, sharedDrainMode=JSC::SlotVisitor::HelperDrain, timeout=<error reading variable: That operation is not available on integers of more than 8 bytes.>) at vendor/WebKit/Source/JavaScriptCore/heap/SlotVisitor.cpp:699 #23 0x000000000aa08726 in JSC::Heap::runBeginPhase(JSC::GCConductor)::$_1::operator()() const (this=<optimized out>) at vendor/WebKit/Source/JavaScriptCore/heap/Heap.cpp:1508 #24 WTF::SharedTaskFunctor<void (), JSC::Heap::runBeginPhase(JSC::GCConductor)::$_1>::run() (this=<optimized out>) at .WTF/Headers/wtf/SharedTask.h:91 #25 0x000000000aa3b596 in WTF::ParallelHelperClient::runTask(WTF::RefPtr<WTF::SharedTask<void ()>, WTF::RawPtrTraits<WTF::SharedTask<void ()> >, WTF::DefaultRefDerefTraits<WTF::SharedTask<void ()> > > const&) (this=0x14f22e000428, task=...) at vendor/WebKit/Source/WTF/wtf/ParallelHelperPool.cpp:110 #26 0x000000000aa3d976 in WTF::ParallelHelperPool::Thread::work (this=<optimized out>) at vendor/WebKit/Source/WTF/wtf/ParallelHelperPool.cpp:201 #27 0x000000000aa4210d in WTF::AutomaticThread::start(WTF::AbstractLocker const&)::$_0::operator()() const (this=<optimized out>) at vendor/WebKit/Source/WTF/wtf/AutomaticThread.cpp:225 #28 WTF::Detail::CallableWrapper<WTF::AutomaticThread::start(WTF::AbstractLocker const&)::$_0, void>::call() (this=<optimized out>) at vendor/WebKit/Source/WTF/wtf/Function.h:53 #29 0x0000000008958ada in WTF::Function<void ()>::operator()() const (this=<optimized out>) at vendor/WebKit/Source/WTF/wtf/Function.h:82 #30 WTF::Thread::entryPoint (newThreadContext=<optimized out>) at vendor/WebKit/Source/WTF/wtf/Threading.cpp:272 #31 0x0000000008a65689 in WTF::wtfThreadEntryPoint (context=0x13b5) at vendor/WebKit/Source/WTF/wtf/posix/ThreadingPOSIX.cpp:255 #32 0x000000000467d347 in asan_thread_start(void*) () #33 0x000014f2c36711f5 in ?? () from /lib/x86_64-linux-gnu/libc.so.6 #34 0x000014f2c36f189c in ?? () from /lib/x86_64-linux-gnu/libc.so.6 ``` </details> `ZSTD_sizeof_CCtx` and `ZSTD_sizeof_DCtx` can not be relied upon to be thread-safe and estimatedSize may be called from any thread
pull Bot
pushed a commit
that referenced
this pull request
Aug 20, 2025
…Worker" (oven-sh#21994) Reverts oven-sh#21962 `vm.ensureTerminationException` allocates a JSString, which is not safe to do from a thread that doesn't own the API lock. ```ts Bun Canary v1.2.21-canary.1 (f706382a) Linux x64 (baseline) Linux Kernel v6.12.38 | musl CPU: sse42 popcnt avx avx2 avx512 Args: "/var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/release/bun-linux-x64-musl-baseline-profile/bun-profile" "/var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/test/js/node/worker_threads"... Features: bunfig http_server jsc tsconfig(3) tsconfig_paths workers_spawned(40) workers_terminated(34) Builtins: "bun:main" "node:worker_threads" Elapsed: 362ms | User: 518ms | Sys: 63ms RSS: 0.34GB | Peak: 100.36MB | Commit: 0.34GB | Faults: 0 | Machine: 8.17GB panic(main thread): Segmentation fault at address 0x0 oh no: Bun has crashed. This indicates a bug in Bun, not your code. To send a redacted crash report to Bun's team, please file a GitHub issue using the link below: http://localhost:38809/1.2.21/Ba2f706382wNgkgUu11luEm6yX+lwy+Dgtt+oEurthoD8214mE___07+09DA2AA 6 | describe("Worker destruction", () => { 7 | const method = ["Bun.connect", "Bun.listen", "fetch"]; 8 | describe.each(method)("bun when %s is used in a Worker that is terminating", method => { 9 | // fetch: ASAN failure 10 | test.skipIf(isBroken && method == "fetch")("exits cleanly", () => { 11 | expect([join(import.meta.dir, "worker_thread_check.ts"), method]).toRun(); ^ error: Command /var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/test/js/node/worker_threads/worker_thread_check.ts Bun.connect failed: Spawned 10 workers RSS 79 MB Spawned 10 workers RSS 87 MB Spawned 10 workers RSS 90 MB at <anonymous> (/var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/test/js/node/worker_threads/worker_destruction.test.ts:11:73) ✗ Worker destruction > bun when Bun.connect is used in a Worker that is terminating > exits cleanly [597.56ms] ✓ Worker destruction > bun when Bun.listen is used in a Worker that is terminating > exits cleanly [503.47ms] » Worker destruction > bun when fetch is used in a Worker that is terminating > exits cleanly 1 pass 1 skip 1 fail 2 expect() calls Ran 3 tests across 1 file. [1125.00ms] ======== Stack trace from GDB for bun-profile-28234.core: ======== Program terminated with signal SIGILL, Illegal instruction. #0 crash_handler.crash () at crash_handler.zig:1523 [Current thread is 1 (LWP 28234)] #0 crash_handler.crash () at crash_handler.zig:1523 #1 0x0000000002db77aa in crash_handler.crashHandler (reason=..., error_return_trace=0x0, begin_addr=...) at crash_handler.zig:471 #2 0x0000000002db2b55 in crash_handler.handleSegfaultPosix (sig=<optimized out>, info=<optimized out>) at crash_handler.zig:792 #3 0x0000000004716b58 in WTF::jscSignalHandler (sig=11, info=0x7ffe54051e90, ucontext=0x0) at vendor/WebKit/Source/WTF/wtf/threads/Signals.cpp:548 #4 <signal handler called> #5 JSC::VM::currentThreadIsHoldingAPILock (this=0x148296c30000) at vendor/WebKit/Source/JavaScriptCore/runtime/VM.h:840 #6 JSC::sanitizeStackForVM (vm=...) at vendor/WebKit/Source/JavaScriptCore/runtime/VM.cpp:1369 #7 0x0000000003f4a060 in JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::{lambda()#1}::operator()() const (this=<optimized out>) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/LocalAllocatorInlines.h:46 #8 JSC::FreeList::allocateWithCellSize<JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::{lambda()#1}>(JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::{lambda()#1} const&, unsigned long) (this=0x148296c38e48, cellSize=16, slowPath=...) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/FreeListInlines.h:46 #9 JSC::LocalAllocator::allocate (this=0x148296c38e30, heap=..., cellSize=16, deferralContext=0x0, failureMode=JSC::AllocationFailureMode::Assert) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/LocalAllocatorInlines.h:44 #10 JSC::GCClient::IsoSubspace::allocate (this=0x148296c38e30, vm=..., cellSize=16, deferralContext=0x0, failureMode=JSC::AllocationFailureMode::Assert) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/IsoSubspaceInlines.h:34 #11 JSC::tryAllocateCellHelper<JSC::JSString, (JSC::AllocationFailureMode)0> (vm=..., size=16, deferralContext=0x0) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/JSCellInlines.h:192 #12 JSC::allocateCell<JSC::JSString> (vm=..., size=16) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/JSCellInlines.h:212 #13 JSC::JSString::create (vm=..., value=...) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/JSString.h:204 #14 0x0000000004479ad1 in JSC::jsNontrivialString (vm=..., s=...) at vendor/WebKit/Source/JavaScriptCore/runtime/JSString.h:846 #15 JSC::VM::ensureTerminationException (this=0x148296c30000) at vendor/WebKit/Source/JavaScriptCore/runtime/VM.cpp:627 #16 JSGlobalObject__requestTermination (globalObject=<optimized out>) at ./build/release/./src/bun.js/bindings/ZigGlobalObject.cpp:3979 #17 0x0000000003405ab8 in bun.js.web_worker.notifyNeedTermination (this=0x542904f0d80) at /var/lib/buildkite-agent/builds/ip-172-31-16-28/bun/bun/src/bun.js/web_worker.zig:558 #18 0x0000000004362b6f in WebCore::Worker::terminate (this=0x984c900000000000) at ./src/bun.js/bindings/webcore/Worker.cpp:266 #19 WebCore::jsWorkerPrototypeFunction_terminateBody(JSC::JSGlobalObject*, JSC::CallFrame*, WebCore::JSWorker*)::{lambda()#1}::operator()() const (this=<optimized out>) at ./build/release/./src/bun.js/bindings/webcore/JSWorker.cpp:549 #20 WebCore::toJS<WebCore::IDLUndefined, WebCore::jsWorkerPrototypeFunction_terminateBody(JSC::JSGlobalObject*, JSC::CallFrame*, WebCore::JSWorker*)::{lambda()#1}>(JSC::JSGlobalObject&, JSC::ThrowScope&, WebCore::jsWorkerPrototypeFunction_terminateBody(JSC::JSGlobalObject*, JSC::CallFrame*, WebCore::JSWorker*)::{lambda()#1}&&) (lexicalGlobalObject=..., throwScope=..., valueOrFunctor=...) at ./src/bun.js/bindings/webcore/JSDOMConvertBase.h:174 #21 WebCore::jsWorkerPrototypeFunction_terminateBody (lexicalGlobalObject=<optimized out>, callFrame=<optimized out>, castedThis=<optimized out>) at ./build/release/./src/bun.js/bindings/webcore/JSWorker.cpp:549 #22 WebCore::IDLOperation<WebCore::JSWorker>::call<&WebCore::jsWorkerPrototypeFunction_terminateBody, (WebCore::CastedThisErrorBehavior)0> (lexicalGlobalObject=..., operationName=..., callFrame=...) at ./src/bun.js/bindings/webcore/JSDOMOperation.h:63 #23 WebCore::jsWorkerPrototypeFunction_terminate (lexicalGlobalObject=<optimized out>, callFrame=0x7ffe540536b8) at ./build/release/./src/bun.js/bindings/webcore/JSWorker.cpp:554 #24 0x000014825580c038 in ?? () #25 0x00007ffe540537b0 in ?? () #26 0x0000148255a626cb in ?? () #27 0x0000000000000000 in ?? () 1 crashes reported during this test ```
pull Bot
pushed a commit
that referenced
this pull request
Apr 28, 2026
…n-sh#29800) ## What does this PR do? Fixes a use-after-free in `selectALPNCallback` (`src/bun.js/api/bun/socket.zig:17`) when a `node:tls` / `Bun.listen` TLS server with `ALPNProtocols` handles overlapping handshakes. `SSL_CTX_set_alpn_select_cb` registers on the **listener-level** `SSL_CTX`, so its `arg` is shared across every accepted connection. The old code passed the per-connection `*TLSSocket` as `arg`, so each `onOpen` overwrote the previous one. When connection A's ClientHello is processed after connection B's `onOpen` has run — and B has since been freed — the callback dereferences a dangling pointer and feeds garbage `protos` into `SSL_select_next_proto`: ``` #9 SSL_select_next_proto (..., peer=0xb10008886384bf6d, peer_len=1431130639, supported="\002h2\bhttp/1.1", supported_len=12) #10 selectALPNCallback (in="\002h2\bhttp/1.1", inlen=12, arg=<freed>) at socket.zig:23 #11 bssl::ssl_negotiate_alpn #12 bssl::do_select_parameters #21 ssl_on_data at openssl.c:505 ``` (from `fetch-http2-client.test.ts` under `describe.concurrent` on `:alpine: 3.23 aarch64`) **Fix:** store the `*TLSSocket` on the per-connection `SSL` via `SSL_set_ex_data(ssl, 0, this)` and read it back from the `SSL*` parameter in the callback, ignoring the CTX-level `arg`. Slot 0 is otherwise unused in the codebase. ## How did you verify your code works? - `bun bd test test/js/node/tls/node-tls-server.test.ts test/js/node/tls/node-tls-connect.test.ts test/js/node/http2/node-http2.test.js test/js/web/fetch/fetch-http2-client.test.ts` → 349 pass, 0 fail - `bun run zig:check-all` → all platforms compile - The existing `connectionListener should emit the right amount of times, and with alpnProtocol available` test (50 parallel ALPN connections) covers the path; the original crash was caught by `fetch-http2-client.test.ts` on aarch64-musl CI
pull Bot
pushed a commit
that referenced
this pull request
May 2, 2026
…es (oven-sh#30077) ## What When a chunked (or HTTP/3) request body exceeds `maxRequestBodySize`, `onBufferedBodyChunk` writes the 413 directly on the raw uWS response: ```zig resp.writeStatus("413 Payload Too Large"); resp.endWithoutBody(comptime !http3); ``` `internalEnd` → `markDone()` nulls `onAborted`, so when the socket closes no abort ever fires to detach `ctx.resp` or release the base ref. `this.resp` is left pointing at a completed response whose socket is about to be freed by `us_internal_free_closed_sockets`. If the fetch handler returned a pending Promise: - **resolve**: `handleResolve` → `isAbortedOrEnded()` is false (`this.resp != null`) → `render()` → `runCorkedWithType` corks the freed socket → **heap-use-after-free** (ASAN trace below). - **reject**: `handleReject` reads `resp.hasResponded()` off freed memory, sees `true`, skips the error handler, and returns without ever releasing the base ref → **RequestContext leaks** (`server.pendingRequests` never returns to 0). ## Fix Route through `this.endWithoutBody()` (the `RequestContext` wrapper) instead of the raw `resp.endWithoutBody()`. That path does `detachResponse()` (nulls `this.resp`, clears `onData`/`onAborted`/`onTimeout`) and `deref()` (releases the base ref), matching every other end path in this file. The body promise is rejected with the specific `"Request body exceeded maxRequestBodySize"` error *before* `endWithoutBody()` so `endRequestStreaming()` doesn't overwrite it with a generic `ConnectionClosed`. `has_written_status` is set so any later `renderMissing`/`renderMetadata` knows the status line is already committed. ## Repro ``` ==ERROR: AddressSanitizer: heap-use-after-free #0 us_socket_group socket.c:77 #1 uWS::AsyncSocket<false>::getLoopData() AsyncSocket.h:69 #2 uWS::AsyncSocket<false>::isCorked() AsyncSocket.h:141 #3 uWS::HttpResponse<false>::cork(...) HttpResponse.h:647 #4 uws_res_cork libuwsockets.cpp:1740 #5 ...runCorkedWithType Response.zig:299 #6 ...doRenderBlob RequestContext.zig:1942 ... #11 ...handleResolve RequestContext.zig:220 #12 ...onResolve RequestContext.zig:154 freed by: #1 us_poll_free epoll_kqueue.c:73 #2 us_internal_free_closed_sockets loop.c:305 ``` ## Test `test/js/bun/http/serve-pending-promise-abort-leak.test.ts` — new case sends a raw `Transfer-Encoding: chunked` POST exceeding `maxRequestBodySize` with a handler that holds its resolve/reject, waits for the socket to be reclaimed, then settles the Promise. Asserts `pendingRequests` returns to 0 for both paths, the body was rejected with the right message, and a follow-up request still works. Without the fix: ASAN heap-use-after-free on the resolve path; on release builds the reject path shows `pendingAfterReject: 1` (leak). Co-authored-by: robobun <robobun@users.noreply.github.com>
pull Bot
pushed a commit
that referenced
this pull request
Jun 23, 2026
…e re-enters the event loop (oven-sh#32597) Sentry BUN-2WJA / BUN-2WKB (~290 events combined, Windows x86_64, `http_server=True`, bun 1.2.23 through 1.3.14): ``` Segmentation fault at address 0xFFFFFFFFFFFFFFFF endWithSink src/runtime/webcore/Sink.zig:577 endFromJS src/runtime/webcore/streams.zig:1200 finalize src/runtime/webcore/streams.zig:1301 clearAndFree src/collections/baby_list.zig:148 memset (fault at 0xFFFFFFFFFFFFFFFF) ``` ## Cause The generated `JSReadable*Controller` `end()` and `close()` host functions (`src/codegen/generate-jssink.ts`) stash `m_sinkPtr` in a local, call `controller->detach()`, and only afterward dereference the stashed pointer via `endWithSink()` / `${name}__close()`: ```cpp void *ptr = controller->wrapped(); controller->detach(); // runs onClose JS synchronously return ${name}__endWithSink(ptr, lexicalGlobalObject); // derefs ptr ``` `detach()` invokes the stored `onClose` callback. For a `type: "direct"` stream this is `readDirectStream`'s `close(stream, reason)`, which calls `underlyingSource.cancel()`. That is arbitrary user code running while `ptr` is still live on the C++ stack. If the stream's `pull()` promise has already settled, `RequestContext::on_resolve_stream` is sitting in the microtask queue. Any path from `cancel()` that drains microtasks (e.g. the server-side drain points in `on_response` / `do_render_with_body`, or an explicit `drainMicrotasks()`) runs `handle_resolve_stream`, which calls `destroy_sink` and frees the `HTTPServerWritable`. `endWithSink(ptr)` then enters `end_from_js` on the freed allocation; `finalize()` reads garbage for `pooled_buffer` / `buffer.cap` / `buffer.ptr` and faults in the `memset` the allocator's free-scrub path performs. The same ordering appears in the Rust port (`streams.rs` / `Sink.rs`) unchanged. ## Fix In `${controller}__end` and `${controller}__close`, finish the native sink operation before any JS runs: 1. Call `${name}__controllerDetached(ptr, controller)` and null `m_sinkPtr` up front (so `end_from_js`'s own `signal.close()` stays a no-op, matching the previous behaviour, and so the later `detach()` won't touch the native side again). 2. Run `endWithSink(ptr)` / `close(ptr)`. 3. Call `controller->detach()` last. With `m_sinkPtr` already null it only clears `m_onPull` and fires `onClose`; by now we hold no reference into the sink, so re-entrant teardown is safe. ## Verification New ASAN-gated test in `test/js/bun/http/serve-direct-readable-stream.test.ts` reproduces the exact UAF deterministically by draining microtasks from the stream's `cancel()` callback (the test uses `require("bun:jsc").drainMicrotasks()` to force the drain that the production crash hits via the server's own drain points). <details> <summary>ASAN output on the unfixed build</summary> ``` ==22203==ERROR: AddressSanitizer: heap-use-after-free on address 0x6ee5f87602ca READ of size 1 at 0x6ee5f87602ca thread T0 #0 HTTPServerWritable::end_from_js src/runtime/webcore/streams.rs:1831 #2 JSSink::js_end_with_sink src/runtime/webcore/Sink.rs:1107 #4 WebCore::JSReadableHTTPResponseSinkController__end JSSink.cpp:620 freed by thread T0 here: #10 HTTPServerWritable::destroy src/runtime/webcore/streams.rs:1950 #11 RequestContext::destroy_sink src/runtime/server/RequestContext.rs:1930 #12 RequestContext::handle_resolve_stream src/runtime/server/RequestContext.rs:2680 #13 RequestContext::on_resolve_stream src/runtime/server/RequestContext.rs:2716 ... #24 JSC::VM::drainMicrotasks() ``` </details> With the fix the fixture completes normally. Existing suites (`serve.test.ts`, `bun-server.test.ts`, `direct-readable-stream.test.tsx`, `streams.test.js`, the sink leak tests) show no new failures against the unfixed build. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
pull Bot
pushed a commit
that referenced
this pull request
Jun 26, 2026
…sweep (oven-sh#32729) ### Crash ``` ASSERTION FAILED: vm().currentThreadIsHoldingAPILock() => vm().heap.mutatorState() != MutatorState::Sweeping vendor/WebKit/Source/JavaScriptCore/runtime/JSCell.cpp(179) : bool JSC::JSCell::validateIsNotSweeping() const ``` Backtrace (from a release-asan build with asserts): ``` #3 JSC::JSCell::validateIsNotSweeping() #4 JSC::JSCell::classInfo() const #5 WTF::uncheckedDowncast<WebCore::JSResumableFetchSink>(JSValue const&) #6 ResumableFetchSinkPrototype__ondrainSetCachedValue #7 bun_runtime::webcore::fetch::fetch_tasklet::FetchTasklet::ignore_remaining_response_body #8 JSC::WeakBlock::sweep() <- inside GC sweep (Weak finalizer) #9 JSC::WeakSet::sweep() #10 JSC::PreciseAllocation::sweep() #12 JSC::Heap::finalize() #21 JSC::LocalAllocator::allocateSlowCase #23 JSC::ErrorInstance::create <- ordinary allocation kicked off GC ``` Found by the syscall fault-injection fuzzer's client-side grammar scenario (fetch/node:http with abort + transient errno on the client socket). Reproduces ~4/5 under `BUN_JSC_collectContinuously=1`. ### Cause `FetchTasklet::on_response_finalize` is the `WeakRefOwner<FetchResponse>::finalize` callback and runs inside `WeakBlock::sweep` while `MutatorState == Sweeping`. When the response body is `Locked` without a pending promise or stream it calls `ignore_remaining_response_body()`, which called: - `ResumableSink::detach_js()`: writes the sink wrapper's cached `ondrain` / `oncancel` / `stream` slots via the generated `ResumableFetchSinkPrototype__*SetCachedValue` helpers. Each does `uncheckedDowncast<JSResumableFetchSink>(thisValue)`, which reaches `JSCell::classInfo()` and then issues a write barrier on the wrapper cell. - `clear_stream_handlers()`: reaches `ReadableStreamTag__tagged` -> `object->inherits<JSReadableStream>()` (guarded today, but one boolean away). Calling `classInfo()` on any cell while the mutator is sweeping is forbidden: the cell's `Structure` may already have been swept. Assert builds catch it; release builds corrupt the heap. ### Fix Thread a `from_finalizer` flag through `ignore_remaining_response_body`. When `true` (the `on_response_finalize` caller) skip `detach_js()` and `clear_stream_handlers()`; only native state is touched. The sink's JS-side detach still happens from `clear_sink()` in `FetchTasklet::deinit()`, which runs as an event-loop `ConcurrentTask` outside any sweep, so nothing leaks. The `on_stream_cancelled_callback` caller (reader `.cancel()`, runs from JS on the event loop) passes `false` and keeps the immediate detach. Also corrects the `ResumableSink::detach_js` doc comment that claimed finalizer safety. ### Verification New test at `test/js/web/fetch/fetch-response-finalizer-sweep.test.ts`: a child process under `BUN_JSC_collectContinuously=1` does 12 iterations of `fetch()` with a user-constructed `ReadableStream` body (so the sink takes the JS route with a Strong `js_this`) against a raw TCP server that sends headers + a partial chunked body and never terminates it, then drops the `Response` unconsumed and runs `Bun.gc(true)`. Without the fix (`bun bd`, src/ stashed): ``` exitCode: 134 stderr: ASSERTION FAILED: vm().currentThreadIsHoldingAPILock() => vm().heap.mutatorState() != MutatorState::Sweeping ``` With the fix: `stdout: "ok"`, `exitCode: 0`. `test/js/web/fetch/fetch-backpressure.test.ts` (exercises the `on_stream_cancelled_callback` path) passes unchanged. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
pull Bot
pushed a commit
that referenced
this pull request
Jun 26, 2026
…oven-sh#32742) ### What does this PR do? Fixes a use-after-free in the HTTP client's CONNECT proxy tunnel, caught by ASAN: ``` READ of size 8 at 0x61e00001fe80 thread T6 #0 Option<RefPtr<ProxyTunnel>>::as_ref #1 proxy_tunnel::on_close ProxyTunnel.rs:525 #2 SSLWrapper::trigger_close_callback uws/lib.rs:833 #3 SSLWrapper::handle_reading uws/lib.rs:1053 ... freed by thread T6 here (same stack, same `handle_reading` call): #5 AsyncHTTP::on_async_http_callback_raw AsyncHTTP.rs:819 #7 HTTPClient::send_progress_update_without_stage_check #9 proxy_tunnel::on_data ProxyTunnel.rs:350 #11 SSLWrapper::trigger_data_callback uws/lib.rs:824 #12 SSLWrapper::handle_reading uws/lib.rs:1046 ``` `SSLWrapper::handle_reading` flushes pending decrypted bytes to the data callback, then runs the close callback, guarded only by `closed_notified`: 1. The flushed data callback completes a keep-alive response through the tunnel. A fatal TLS record error sets only `fatal_error` — none of the shutdown flags — so the wrapper passed `tunnel_poolable`'s `!is_shutdown()` check and the tunnel was handed to the keep-alive pool. Nothing called `wrapper.shutdown()`, so `closed_notified` was never latched. Dispatching the final result then freed the `ThreadlocalAsyncHTTP` that embeds the `HTTPClient`. 2. The guard (`ssl.is_none() || closed_notified()`) passes. 3. `trigger_close_callback()` invokes `on_close(handlers.ctx)` with `ctx` pointing at the freed client. The pooling branch is the only terminal path that doesn't go through `close_proxy_tunnel(true)` → `wrapper.shutdown()` → `closed_notified`, which is the latch the read loop relies on. `SSLWrapper::shutdown` already special-cases the *close_notify* flavor of this for exactly that reason; the fatal-error flavor never reaches `shutdown()`. The fix is one predicate: a tunnel whose wrapper has a fatal error or pending unconsumed input/output is not poolable. That routes it through the orderly teardown that latches `closed_notified`, and the pending-I/O half closes the same hole for a tunnel pooled from a mid-loop data callback while more decrypted bytes or queued output remain. Both are also required for the pool to be correct on its own terms — a poisoned or dirty TLS session must not be handed to the next request. ### How did you verify your code works? New regression test in `test/js/bun/http/proxy.test.ts` (next to the existing close_notify sibling): an HTTPS keep-alive response through a CONNECT proxy with a corrupt TLS record appended to the same TCP burst, followed by a second request that can only complete if the HTTP client thread survived the first. Against an unfixed ASAN debug build the fixture aborts every run: ``` ==20981==ERROR: AddressSanitizer: heap-use-after-free on address 0x61e00001fe80 READ of size 8 at 0x61e00001fe80 thread T6 ... exit=134 ``` With this change it prints `4096 200 200` and exits 0 with no ASAN report. `test/js/bun/http/proxy.test.ts` (49/49), `fetch-proxy-connect-tunnel-split-envelope.test.ts`, `fetch-proxy-tls-intern-race.test.ts`, and `fetch-keepalive.test.ts` all pass.
pull Bot
pushed a commit
that referenced
this pull request
Jul 13, 2026
…wn (oven-sh#34035) Fixes `test/bake/dev/request-cookies.test.ts` going red on the `debian 13 x64-asan` lane (seen in [build 72183](https://buildkite.com/bun/bun/builds/72183#019f55d5-6839-41cf-b0f5-3c56ada43ef9) and [build 71964](https://buildkite.com/bun/bun/builds/71964)): ``` dev| ==1715==ERROR: AddressSanitizer: SEGV on unknown address 0x000000007490 error: DevServer panicked at gracefulExit (test/bake/bake-harness.ts:614:17) ✗ DEV:request-cookies-1: request.cookies.get() basic functionality ``` ### Cause `~DevServerSourceProvider` held a raw `Zig::GlobalObject*` and called `m_globalObject->bunVM()` to reach `Bun__removeDevServerSourceProvider`. Under `BUN_DESTRUCT_VM_ON_EXIT=1` (set by the CI runner for the asan lane), the harness's `process.exit(0)` runs `Zig__GlobalObject__destructOnExit`, which does `gcUnprotect(globalObject)` then `collectNow(Sync, Full)` then two `vm.derefSuppressingSaferCPPChecking()`. The global object cell is swept during `collectNow`, but the provider's last `Ref` is only released later from `~CodeCache` inside `~JSC::VM`, so the destructor read `m_bunVM` out of a freed cell. With bmalloc the freed cell usually still holds the old value and the read happens to work, which is why this was ~0.5% in CI and never reproduced locally. When the memory is reused with a zero at that offset the Rust side receives a null `VirtualMachine*` and the next access is `(null)->source_mappings.mutex`, which lands at exactly 0x7490. Deterministic ASAN backtrace with `Malloc=1`: ``` ==79839==ERROR: AddressSanitizer: heap-use-after-free ... #0 Zig::GlobalObject::bunVM() const ZigGlobalObject.h:353 #1 Bake::DevServerSourceProvider::~DevServerSourceProvider() DevServerSourceProvider.h:65 ... #7 JSC::SourceCodeKey::~SourceCodeKey() #12 JSC::CodeCacheMap::~CodeCacheMap() #16 JSC::VM::~VM() #18 Zig__GlobalObject__destructOnExit ZigGlobalObject.cpp:4049 #19 VirtualMachine::global_exit VirtualMachine.rs:1603 #20 Bun__Process__exit ``` ### Fix Store the Rust `VirtualMachine*` directly (`void* m_bunVM`), captured in `create()`, so the destructor no longer indirects through a GC cell. This mirrors `Zig::SourceProvider`, which already stores `m_bunVM` for the same reason. The Rust `VirtualMachine` outlives every GC cell (step 10 of `global_exit` is `self.destroy()`, after `destructOnExit` has finished). ### Verification New ASAN-only case in `test/bake/dev/server-sourcemap.test.ts` runs the dev server with `Malloc=1` + `BUN_DESTRUCT_VM_ON_EXIT=1` so ASAN poisons the swept global-object cell, making the UAF deterministic. Added an `env` option to the `devTest` harness so the test can set those for the spawned dev server. ``` # fail-before (src/ stashed) SUMMARY: AddressSanitizer: heap-use-after-free ZigGlobalObject.h:353:48 in Zig::GlobalObject::bunVM() const (fail) DEV:server-sourcemap-5: DevServerSourceProvider destructor does not touch the swept global object on process exit # pass-after (pass) DEV:server-sourcemap-5: DevServerSourceProvider destructor does not touch the swept global object on process exit ``` `test/bake/dev/server-sourcemap.test.ts` (5 tests) and `test/bake/dev/request-cookies.test.ts` (2 tests) are green. `request-cookies.test.ts` now also passes under the full CI LeakSan config (`BUN_DESTRUCT_VM_ON_EXIT=1` + `detect_leaks=1`). The bug is from a89e61f (oven-sh#22138), which introduced `DevServerSourceProvider` with the raw global-object pointer. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 1 · 3 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 1 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/request-cookies.test.ts test/bake/dev/server-sourcemap.test.ts info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05) info: component rust-src is up to date info: checking for self-update (current version: 1.29.0) bun test v1.4.0 (722d6f0) test/bake/dev/server-sourcemap.test.ts: Dev server testing directory: /tmp/bun-dev-test-tI2Y82 bun add v1.4.0 (722d6f0) Resolving dependencies Resolved, downloaded and extracted [2] Saved lockfile installed react@0.0.0-experimental-603e6108-20241029 installed react-dom@0.0.0-experimental-603e6108-20241029 installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029 installed react-refresh@0.0.0-experimental-603e6108-20241029 6 packages installed [462.00ms] bun install v1.4.0 (722d6f0) Checked 6 installs across 7 packages (no changes) [167.00ms] �[0;30mdev|�[0m Started development server: http://localhost:37377 �[0;30mdev|�[0m �[32mBundled page in 2125ms�[0m�[2m:�[0 ... (truncated) release without fix: all passed bun test v1.4.0-canary.1 (1498d7b) test/bake/dev/server-sourcemap.test.ts: Dev server testing directory: /tmp/bun-dev-test-7LPeWv bun add v1.4.0-canary.1 (1498d7b) Resolving dependencies Resolved, downloaded and extracted [0] Saved lockfile installed react@0.0.0-experimental-603e6108-20241029 installed react-dom@0.0.0-experimental-603e6108-20241029 installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029 installed react-refresh@0.0.0-experimental-603e6108-20241029 6 packages installed [9.00ms] bun install v1.4.0-canary.1 (1498d7b) Checked 6 installs across 7 packages (no changes) [0.00ms] �[0;30mdev|�[0m Started development server: http://localhost:43275 �[0;30mdev|�[0m �[32mBundled page in 47ms�[0m�[2m:�[0m pages/[...slug].tsx �[2m+ 2 more�[0m �[0;30mdev|�[0m �[0m�[1m1 |�[0m �[0m�[35mexport�[0m �[0m�[35mdefault�[0m �[0m�[35masync�[0m �[0m�[35mfunction�[0m MyPage(params) { �[0;30mdev|�[0m �[0m�[1m2 |�[0m myFunc()�[0m�[2m;�[0m �[0;30mdev|�[0m �[0m�[1m3 |�[0m �[0m�[35mreturn�[0m �[0m<�[0mh1>{JSON�[0m�[3m�[1m.stringify�[0m(params)}�[0m<�[0m/h1>�[0m�[2m;�[0m �[0;30mdev|�[0m �[0m�[1m4 |�[0m } �[0;30mdev|�[0m �[0m�[1m5 |�[0m �[0;30mdev|�[0m �[0m� ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/request-cookies.test.ts test/bake/dev/server-sourcemap.test.ts info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05) info: component rust-src is up to date info: checking for self-update (current version: 1.29.0) bun test v1.4.0 (722d6f0) test/bake/dev/server-sourcemap.test.ts: Dev server testing directory: /tmp/bun-dev-test-dkR1se bun add v1.4.0 (722d6f0) Resolving dependencies Resolved, downloaded and extracted [0] Saved lockfile installed react@0.0.0-experimental-603e6108-20241029 installed react-dom@0.0.0-experimental-603e6108-20241029 installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029 installed react-refresh@0.0.0-experimental-603e6108-20241029 6 packages installed [119.00ms] bun install v1.4.0 (722d6f0) Checked 6 installs across 7 packages (no changes) [97.00ms] �[0;30mdev|�[0m Started development server: http://localhost:44249 �[0;30mdev|�[0m �[32mBundled page in 2351ms�[0m�[2m:�[0m ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05) info: component rust-src is up to date info: checking for self-update (current version: 1.29.0) [configured] bun-profile → bun (stripped) in 690ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [0/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05) info: component rust-src is up to date info: component rust-std is up to date nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05) info: checking for self-update (current version: 1.29.0) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/runtime/bake/DevServerSourceProvider.h | 13 +++++++----- test/bake/bake-harness.ts | 5 +++++ test/bake/dev/server-sourcemap.test.ts | 34 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/runtime/bake/DevServerSourceProvider.h 1 2 0 test/bake/bake-harness.ts 8 2 0 test/bake/dev/server-sourcemap.test.ts 1 4 0 ``` </details> <!-- robobun:evidence:end -->
pull Bot
pushed a commit
that referenced
this pull request
Jul 16, 2026
…ry rewrite (oven-sh#34271) `test/js/bun/util/filesystem_router.test.ts` went red on alpine x64 in build [73276](https://buildkite.com/bun/bun/builds/73276): the `reload() while Bun.build() resolves the same directory` subprocess segfaulted in `bust_dir_cache_recursive`, inlined from `NonNull::new`. ## Cause `RealFS::entries_at` (`src/resolver/lib.rs`) replaces a cached `DirEntry` in place when the caller's resolver generation is newer than the cached listing's. The replacement at `*e_ptr = new_entry` drops the old `DirEntry`, which drops its `data: StringHashMap<*mut Entry>` and frees the hashmap's bucket allocation. The function's comment says `entries_mutex held by caller`, but that is only true on one of the five paths that reach it: `dir_info_uncached`, when entered from `dir_info_cached_miss`. The other callers (`finalize_result`, `handle_esm_resolution`, `load_index_with_extension`, `Transpiler::run_env_loader`) all reach `entries_at` after `dir_info_cached_maybe_log` has already returned and released both `RESOLVER_MUTEX` and `entries_mutex`. `FileSystemRouter::reload()` and `RouteLoader::load` iterate the same `DirEntry.data` map under `entries_mutex` (the snapshot pattern oven-sh#33056 introduced for exactly this kind of concurrent rewrite). With `entries_at`'s rewrite unsynchronized, a `Bun.build()` on the bundler thread can drop the map while `reload()` on the JS thread is mid-iteration. The generation mismatch is what makes `entries_at` enter its rewrite branch, so the window only opens once the bundle thread has processed at least one batch (it bumps its own generation after every queue drain); every subsequent `Bun.build()` then re-reads any directory that `reload()` just refreshed to generation 0. ASAN catches it as a heap-use-after-free with the two sides of the race laid out exactly: ``` READ of size 16 (thread T0): #6 HashMap::values #7 StringHashMap<*mut Entry>::values src/collections/array_hash_map.rs:1864 #8 FileSystemRouter::bust_dir_cache_recursive src/runtime/api/filesystem_router.rs:395 #9 FileSystemRouter::bust_dir_cache src/runtime/api/filesystem_router.rs:451 #10 FileSystemRouter::reload src/runtime/api/filesystem_router.rs:476 freed by thread T11 (Bundler): #11 drop_in_place<bun_resolver::fs_full::DirEntry> #12 bun_resolver::fs::RealFS::entries_at src/resolver/lib.rs:1639 #13 DirInfo::get_entries_ref src/resolver/dir_info.rs:266 #14 Resolver::finalize_result src/resolver/resolver.rs:1714 #15 Resolver::resolve_and_auto_install src/resolver/resolver.rs:1485 ... #23 BundleThread::generate_in_new_thread src/bundler/BundleThread.rs:276 previously allocated by thread T0: #17 HashMap::reserve #18 Resolver::dir_info_cached_miss src/resolver/resolver.rs:4591 #19 Resolver::dir_info_cached_maybe_log src/resolver/resolver.rs:4201 #20 Resolver::read_dir_info src/resolver/resolver.rs:4118 #21 FileSystemRouter::reload src/runtime/api/filesystem_router.rs:492 ``` (The use side is sometimes `RouteLoader::load` at `src/router/lib.rs:816` instead; same map, same lock.) This has been the shape of `entries_at` since the Rust port; oven-sh#33056 narrowed the race by snapshotting under the lock but assumed the rewrite side already held it. ## Fix `entries_at` now takes `entries_mutex` itself, matching `read_directory_with_iterator` which already does. The one call path that reaches it with the lock already held (`dir_info_cached_miss` -> `dir_info_uncached` -> `parent_.get_entries_ref`) routes through a new `entries_at_locked` / `get_entries_ref_locked` pair so the non-recursive mutex is not re-entered. That path is the only one that passes a non-`None` parent to `dir_info_uncached`; the other caller (`dir_info_for_resolution`) passes `None`, so the parent branch containing the accessor never runs there. ## Test The existing concurrency test now awaits one `Bun.build()` first, so the bundle thread's generation is already past zero when the concurrent rounds start, and then runs forty reload/build rounds instead of one. That is the shape that reaches the stale-generation rewrite at all; the original single-round fixture usually completes with every build still on generation 0. The race is scheduling-dependent. Pinning the fixture to a single core reproduces the ASAN use-after-free on roughly 3 in 10 runs against an unpatched debug build and 0 in 15 with this change; with all 16 cores available the unpatched build reproduces at roughly 1 in 30. The assertions are otherwise the same as before, so the test continues to cover the behavior oven-sh#33056 added. Also ran the full `filesystem_router.test.ts`, `test/bundler/bun-build-api.test.ts` (including the thousands-of-builds test that exercises the generation path heavily), `test/js/bun/resolve/resolve.test.ts`, `test/cli/hot/hot.test.ts`, `test/cli/watch/watch.test.ts`, `test/bake/framework-router.test.ts`, and `bun run rust:check-all` (10/10 targets). <!-- robobun:evidence:begin --> --- **no test proof** · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/filesystem_router.test.ts <!-- robobun:evidence:end -->
pull Bot
pushed a commit
that referenced
this pull request
Jul 16, 2026
…e drain (oven-sh#34278) ## Problem `test/js/node/test/parallel/test-worker-stdio-flush.js` went red on the `debian 13 x64-asan` lane of [build 73374](https://buildkite.com/bun/bun/builds/73374) with: ``` ==18202==ERROR: LeakSanitizer: detected memory leaks Direct leak of 32 byte(s) in 1 object(s) allocated from: #9 ConcurrentTask::new src/event_loop/ConcurrentTask.rs:305 #10 ConcurrentTask::create src/event_loop/ConcurrentTask.rs:319 #12 bun_jsc::virtual_machine_exports::queue_task_concurrently src/jsc/virtual_machine_exports.rs:140 #13 ScriptExecutionContext::postTaskConcurrently src/jsc/bindings/ScriptExecutionContext.cpp:266 #14 ScriptExecutionContext::postTaskTo src/jsc/bindings/ScriptExecutionContext.cpp:125 #15 MessagePortPipe::scheduleDrain src/jsc/bindings/webcore/MessagePortPipe.cpp:74 #16 MessagePort::postMessage src/jsc/bindings/webcore/MessagePort.cpp:143 ``` The leaked allocation is a `ConcurrentTask` (and the `EventLoopTask` it wraps) left in an exiting worker's `concurrent_tasks` queue after the queue has been drained for the last time. ## Cause `WebWorker::shutdown()` runs `process.on('exit')` handlers, then drains the worker's concurrent queue via `release_queued_tasks_for_shutdown()`, then enters `WebWorker__teardownJSCVM` which (first thing) calls `ctx->markTerminating()`. `ScriptExecutionContext::postTaskTo` already refuses to enqueue onto a terminating context, but between the drain and the flag flip there is a short window where a cross-thread poster still sees `isTerminating() == false` and enqueues. In the failing test the worker writes to `process.stdout` inside its `exit` handler. The parent's captured-stdout reader acks each chunk with `port.postMessage(true)` (`src/js/node/worker_threads.ts` `makePortReadable._read`), which routes through `MessagePortPipe::scheduleDrain` to `postTaskTo(workerCtxId, ...)`. When the ack lands in that window it is pushed onto the worker's `concurrent_tasks`; nothing drains it again, and the worker's VM box is `dealloc`'d raw, so LSan reports the `ConcurrentTask` as a direct leak. The window is a few assignments plus one FFI call wide, so it hits probabilistically; the `release-asan` build is fast enough to line up occasionally, debug essentially never. The ordering was introduced in oven-sh#31216; oven-sh#29917 described the same gap ("a task posted between this drain and `removeFromContextsMap()` inside `teardownJSCVM` still leaks") but left it open. ## Fix - `ScriptExecutionContext::markTerminating()` now takes `allScriptExecutionContextsMapLock`, the same lock `postTaskTo` holds across its `isTerminating()` check and `postTaskConcurrently()` enqueue. That makes the flag flip a proper fence against concurrent posters: any `postTaskTo` critical section either runs entirely before `markTerminating()` (its task is visible to the subsequent drain) or entirely after (it observes `true` and drops). - `WebWorker::shutdown()` calls the new `extern "C" ScriptExecutionContext__markTerminating` immediately before `release_queued_tasks_for_shutdown()`, closing the window. The later `markTerminating()` inside `WebWorker__teardownJSCVM` is now redundant but harmless. No behaviour change for `process.on('exit')` itself: that runs before the new call, so a parent ack posted while the handler is running is still enqueued and then freed by the drain (never executed, same as before). Only posts that would have landed after the drain are now dropped instead of leaked. ## Verification The gap is too narrow to reproduce unassisted against a debug build: 200 iterations of the Node test with the CI LSan env, and 150 worker shutdowns with 64 Atomics-synchronized MessagePorts each, all pass on an unpatched `bun bd`. Widening the gap with a temporary `std::thread::sleep(5ms)` between `release_queued_tasks_for_shutdown()` and `WebWorker__teardownJSCVM` makes it deterministic: | build | `test-worker-stdio-flush.js` under LSan | 200-port Atomics-synchronized probe | | --- | --- | --- | | unpatched + 5 ms sleep | 5/5 leak (`32 byte(s) ConcurrentTask`) | 5/5 leak | | this PR + 5 ms sleep | 10/10 clean | 5/5 clean | | this PR (no sleep) | 50/50 clean | clean | `test/js/node/worker_threads/worker-shutdown-post-leak.test.ts` runs the worker-stdio-on-exit scenario under `detect_leaks=1` as an ASAN-lane guard (in a fresh file so it actually runs; `worker_destruction.test.ts` is ASAN-quarantined via `test/expectations.txt`). The race is not observable on the debug gate without `src/` instrumentation, so the fail-before half will not fire there; `test-worker-stdio-flush.js` on the release-asan lane remains the primary signal. Related: oven-sh#31216 (introduced the ordering), oven-sh#29917 (described but left the remaining window). <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/worker_threads/worker-shutdown-post-leak.test.ts <!-- robobun:evidence:end -->
pull Bot
pushed a commit
that referenced
this pull request
Jul 23, 2026
…cation (oven-sh#35144) ## What `http_proxy=HTTP://host:port` (or any scheme not spelled in lowercase) rejected every request through `fetch` and `bun install` with `UnsupportedProxyProtocol`, while the same string passed via `fetch(url, { proxy: "HTTP://..." })` worked. Similarly, a server responding with `Location: HTTPS://host/...` failed the redirect with `UnsupportedRedirectProtocol`. ## Why RFC 3986 section 3.1 defines the URL scheme as case-insensitive, and both curl and undici's `EnvHttpProxyAgent` accept the uppercase form. The `{ proxy }` option path goes through the WHATWG URL parser, which lowercases the scheme; the `http_proxy` / `HTTPS_PROXY` environment variables are parsed by `bun_url::URL::parse`, which is a borrowing parser and keeps `protocol` as a raw slice of the input. The proxy protocol check in `HTTPThread` and the `is_http()`/`is_https()` helpers compared those bytes exactly. The redirect follower slices the scheme out of the raw `Location` header bytes before WHATWG normalization runs and compared the same way. ## Fix - `bun_url::URL::is_http`, `is_https`, `is_s3`, `is_file`, and `has_http_like_protocol` now compare ASCII case-insensitively. - The two inline scheme checks in `HTTPThread` go through `has_http_like_protocol()`. - The two `Location` scheme comparisons in the redirect follower go through `strings::eql_case_insensitive_ascii`. This also fixes `get_port_auto()` defaulting `HTTPS://proxy` (no explicit port) to 80 instead of 443, and `HTTPClient::is_https()` picking the plaintext context for an `HTTPS://` proxy. ## Verification ``` $ USE_SYSTEM_BUN=1 bun test test/js/bun/http/proxy.test.ts -t "http_proxy env var scheme" (fail) http_proxy=HTTP://... is accepted error: UnsupportedProxyProtocol fetching "http://127.0.0.1:.../x" 1 pass / 3 fail $ USE_SYSTEM_BUN=1 bun test test/js/web/fetch/fetch-redirect.test.ts -t "Location scheme" (fail) Location: HTTP://... error: UnsupportedRedirectProtocol fetching "http://127.0.0.1:.../start" 0 pass / 3 fail $ bun bd test test/js/bun/http/proxy.test.ts -t "http_proxy env var scheme" 4 pass / 0 fail $ bun bd test test/js/web/fetch/fetch-redirect.test.ts -t "Location scheme" 3 pass / 0 fail ``` Full `proxy.test.ts` (62 tests) and `fetch-redirect.test.ts` (15 tests) pass. Related: oven-sh#16182 (this covers scheme case only; full WHATWG normalization of the proxy env URL is still open) <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 0 · 5 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 6 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/proxy.test.ts test/js/web/fetch/fetch-redirect.test.ts bun test v1.4.0 (c50a91c) test/js/bun/http/proxy.test.ts: (pass) GET non-TLS proxy -> non-TLS body type undefined [853.99ms] (pass) POST non-TLS proxy -> non-TLS body type string [836.44ms] (pass) GET TLS proxy -> non-TLS body type undefined [985.30ms] (pass) GET non-TLS proxy -> TLS body type undefined [1142.59ms] (pass) POST non-TLS proxy -> TLS body type string [1177.59ms] (pass) POST TLS proxy -> non-TLS body type string [525.95ms] (pass) GET TLS proxy -> TLS body type undefined [752.01ms] (pass) POST TLS proxy -> TLS body type string [769.62ms] (pass) proxy can handle redirects with non-TLS server > with empty body oven-sh#12007 [937.61ms] (pass) proxy can handle redirects with non-TLS server > with body oven-sh#12007 [1119.27ms] (pass) proxy can handle redirects with TLS server > with empty body oven-sh#12007 [1255.51ms] (pass) proxy can handle redirects with TLS server > with body oven-sh#12007 [1104.45ms] (pass) proxy can handle redirects with non-TLS server > with chunked body #12 ... (truncated) release without fix: 3 failed, 1 skipped bun test v1.4.0-canary.1 (6930da6) test/js/bun/http/proxy.test.ts: (pass) POST non-TLS proxy -> non-TLS body type string [33.08ms] (pass) GET non-TLS proxy -> non-TLS body type undefined [33.34ms] (pass) POST TLS proxy -> non-TLS body type string [38.45ms] (pass) GET TLS proxy -> non-TLS body type undefined [38.48ms] (pass) POST non-TLS proxy -> TLS body type string [41.92ms] (pass) GET non-TLS proxy -> TLS body type undefined [44.32ms] (pass) GET TLS proxy -> TLS body type undefined [49.48ms] (pass) POST TLS proxy -> TLS body type string [49.48ms] (pass) proxy can handle redirects with non-TLS server > with empty body oven-sh#12007 [52.35ms] (pass) proxy can handle redirects with non-TLS server > with body oven-sh#12007 [53.21ms] (pass) proxy can handle redirects with TLS server > with body oven-sh#12007 [56.46ms] (pass) proxy can handle redirects with TLS server > with empty body oven-sh#12007 [58.78ms] (pass) proxy can handle redirects with non-TLS server > with chunked body oven-sh#12007 [650.88ms] (pass) proxy can handle redirects with TLS server > with chunked body oven-sh#12007 [649.97ms] (pass) non-TLS origin redirect through HTTPS proxy forwards every hop through the proxy [8.02ms] (pass) unsupp ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/proxy.test.ts test/js/web/fetch/fetch-redirect.test.ts bun test v1.4.0 (c50a91c) test/js/bun/http/proxy.test.ts: (pass) GET non-TLS proxy -> non-TLS body type undefined [759.01ms] (pass) POST non-TLS proxy -> non-TLS body type string [742.07ms] (pass) GET TLS proxy -> non-TLS body type undefined [934.80ms] (pass) GET non-TLS proxy -> TLS body type undefined [995.92ms] (pass) POST non-TLS proxy -> TLS body type string [1016.42ms] (pass) POST TLS proxy -> non-TLS body type string [401.94ms] (pass) GET TLS proxy -> TLS body type undefined [650.76ms] (pass) POST TLS proxy -> TLS body type string [572.68ms] (pass) proxy can handle redirects with non-TLS server > with empty body oven-sh#12007 [817.57ms] (pass) proxy can handle redirects with non-TLS server > with body oven-sh#12007 [923.98ms] (pass) proxy can handle redirects with TLS server > with empty body oven-sh#12007 [1008.17ms] (pass) proxy can handle redirects with TLS server > with body oven-sh#12007 [949.15ms] (pass) proxy can handle redirects with non-TLS server > with chunked body oven-sh#12007 ... (truncated) release with fix: 1 skipped $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 695ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp) �[1m�[92m Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output) �[1m�[92m Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap) �[1m�[92m Compiling�[0m bun_valkey v0 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/http/HTTPThread.rs | 7 +-- src/http/lib.rs | 20 ++++++-- src/url/lib.rs | 12 +++-- test/js/bun/http/proxy.test.ts | 87 ++++++++++++++++++++++++++++++++ test/js/web/fetch/fetch-redirect.test.ts | 46 +++++++++++++++++ 5 files changed, 159 insertions(+), 13 deletions(-) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/http/HTTPThread.rs 1 2 0 src/http/lib.rs 3 2 0 src/url/lib.rs 3 3 0 test/js/bun/http/proxy.test.ts 1 1 0 test/js/web/fetch/fetch-redirect.test.ts 1 1 0 ``` </details> <!-- robobun:evidence:end -->
pull Bot
pushed a commit
that referenced
this pull request
Jul 29, 2026
…ed (oven-sh#36247) ## What `test/js/bun/http/bun-serve-html.test.ts` segfaults on `windows-aarch64` after oven-sh#36175 landed (builds 84162, 84194; one earlier sighting in 83933): ``` panic(main thread): Segmentation fault at address 0x48 Features: ... dev_server(14) ... ``` Symbolicated in oven-sh#36214 as `AsyncFSTask<Access>::run_from_js_thread` with `self = null`, i.e. a zeroed `ConcurrentTask` was dispatched. ## Cause `DevServer.watcher_atomics.events[*].concurrent_task` is the intrusive MPSC node the watcher thread links into `EventLoop.concurrent_tasks` when it submits a hot-reload event. It was an inline field of `DevServer`, so `server.stop()` → `drop(Box<DevServer>)` freed it while it was still linked. The next `tick_concurrent` then read `.next`/`.task`/`.auto_delete` from freed memory. ASAN on Linux confirms: ``` heap-use-after-free: ConcurrentTask::get_next (unbounded_queue.rs) ← BatchIterator::next ← EventLoop::tick_concurrent_with_count freed by: Box<DevServer>::drop ← NewServer::deinit_if_we_can ← NewServer::stop ← dispose_from_js (using server) ``` On release builds the freed block reads back as zeros, so the copied `Task` is `{tag: 0, ptr: null}`; tag 0 is `task_tag::Access`, whose `run_from_js_thread` loads `self.result` at offset `0x48`. The bug is latent and platform-agnostic. oven-sh#36175 exposed it because the CI runner now spawns the napi addon prebuild in the background while serial tests run; that writes under the watched project root, so the `jsx-runtime` DevServers in this test file now reliably receive a hot-reload event between the last `await fetch` and `using server` disposal. ## Fix `watcher_atomics` is now a `NonNull<WatcherAtomics>` owned via `bun_core::heap::into_raw`, so the allocation can outlive `DevServer` and every queued pointer keeps allocation-root provenance. `watcher_acquire_event`, `watcher_release_and_submit_event` and `recycle_event_from_dev_server` take `*mut Self` and derive the returned `*mut HotReloadEvent` (and the linked `concurrent_task` node) from that root pointer via raw place projections rather than from a `&mut WatcherAtomics` reborrow. `Drop for DevServer` reads `next_event` after `Watcher::shutdown` has serialised out the watcher thread (which guarantees it is stable): - `DONE`: nothing is queued; clear and `heap::destroy` as before. - otherwise: a `concurrent_task` is still linked (or its `Task` is already in the drain FIFO). Null `owner` on every event and leave the allocation alive. `HotReloadEvent::run` checks `owner.is_null()` first; when set it reclaims the allocation via the new `atomics` backref and returns without touching the dead `DevServer`. The `# Safety` contracts on `run` and the `BakeHotReloadEvent` dispatch arm are updated to describe the null-owner case. ## Test `test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts` creates a development server, bundles once so `app.js` is watched, synchronously rewrites `app.js`, spins briefly without yielding so the watcher thread can enqueue, disposes the server, then yields. Ten iterations. In a separate file because the React-bundling cases in `bun-serve-html.test.ts` already exceed the default per-test timeout under a debug+ASAN build on `main`. <details><summary>fail-before (debug+ASAN, src/ at main)</summary> ``` ==25521==ERROR: AddressSanitizer: heap-use-after-free on address 0x79315e4743e8 READ of size 8 at 0x79315e4743e8 thread T0 #2 <ConcurrentTask as Node>::get_next unbounded_queue.rs:82 #3 BatchIterator<ConcurrentTask>::next unbounded_queue.rs:135 #4 EventLoop::tick_concurrent_with_count event_loop.rs:507 0x79315e4743e8 is located 488 bytes inside of 16512-byte region freed by thread T0 here: #9 Box<DevServer>::drop #12 NewServer<false,true>::deinit_if_we_can mod.rs:1770 #13 NewServer<false,true>::stop mod.rs:1665 #14 NewServer<false,true>::dispose_from_js server_body.rs:2584 ``` </details> Passes with the fix in ~2.4s under debug+ASAN (also on a local `windows-aarch64` debug build, where the original `bun-serve-html.test.ts` is now 19/19); `test/bake/deinitialization.test.ts` still green. Supersedes the producer half of oven-sh#36214 (which adds a sentinel for the same zeroed-task symptom). <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 4 · 6 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 1 failed, 2 skipped $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-html.test.ts test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts bun test v1.4.0 (5f6622f) test/js/bun/http/bun-serve-html.test.ts: waitForServer /tmp/html-css-js_ObkyZk { "/": "/tmp/html-css-js_ObkyZk/index.html", "/dashboard": "/tmp/html-css-js_ObkyZk/dashboard.html", } [0.12ms] bundle index.html 1.09 KB [0.05ms] bundle dashboard.html 1.27 KB (pass) serve html [630.67ms] waitForServer /tmp/bun-serve-html-txt_5C6B7a { "/": "/tmp/bun-serve-html-txt_5C6B7a/index.html", } [0.15ms] bundle index.html 0.40 KB HASH efbnbska (pass) serve plugins > basic plugin [556.20ms] waitForServer /tmp/html-css-js-failing-plugin_OPRhwb { "/": "/tmp/html-css-js-failing-plugin_OPRhwb/index.html", } error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_OPRhwb/styles.css:0 error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_OPRhwb/styles.css:0 (pass) serve plugins > serve html with failing plugin [491.35ms] waitForServer /tmp/html-css-js-empty-plugins_biqnN6 { "/": "/tmp/htm ... (truncated) release without fix: all passed bun test v1.4.0-canary.1 (96ff7ec) test/js/bun/http/bun-serve-html.test.ts: waitForServer /tmp/html-css-js_ZmgkBG { "/": "/tmp/html-css-js_ZmgkBG/index.html", "/dashboard": "/tmp/html-css-js_ZmgkBG/dashboard.html", } [0.00ms] bundle index.html 1.09 KB [0.00ms] bundle dashboard.html 1.27 KB (pass) serve html [25.17ms] waitForServer /tmp/bun-serve-html-txt_uWNogT { "/": "/tmp/bun-serve-html-txt_uWNogT/index.html", } [0.00ms] bundle index.html 0.40 KB HASH efbnbska (pass) serve plugins > basic plugin [17.25ms] waitForServer /tmp/html-css-js-failing-plugin_Kd1a8p { "/": "/tmp/html-css-js-failing-plugin_Kd1a8p/index.html", } error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_Kd1a8p/styles.css:0 error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_Kd1a8p/styles.css:0 (pass) serve plugins > serve html with failing plugin [16.33ms] waitForServer /tmp/html-css-js-empty-plugins_Ecz7qJ { "/": "/tmp/html-css-js-empty-plugins_Ecz7qJ/index.html", } [0.00ms] bundle index.html 0.71 KB (pass) serve plugins > empty plugin array [13.23ms] Waiting for server waitForServer /tmp/html-css-js-concurrent-plugins_l7wQg5 { "/": "/tmp/ ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: 2 skipped $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-html.test.ts test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts bun test v1.4.0 (5f6622f) test/js/bun/http/bun-serve-html.test.ts: waitForServer /tmp/html-css-js_CpXhx4 { "/": "/tmp/html-css-js_CpXhx4/index.html", "/dashboard": "/tmp/html-css-js_CpXhx4/dashboard.html", } [0.09ms] bundle index.html 1.09 KB [0.05ms] bundle dashboard.html 1.27 KB (pass) serve html [588.64ms] waitForServer /tmp/bun-serve-html-txt_rjZL4e { "/": "/tmp/bun-serve-html-txt_rjZL4e/index.html", } [0.15ms] bundle index.html 0.40 KB HASH efbnbska (pass) serve plugins > basic plugin [552.11ms] waitForServer /tmp/html-css-js-failing-plugin_D8NJ2O { "/": "/tmp/html-css-js-failing-plugin_D8NJ2O/index.html", } error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_D8NJ2O/styles.css:0 error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_D8NJ2O/styles.css:0 (pass) serve plugins > serve html with failing plugin [505.55ms] waitForServer /tmp/html-css-js-empty-plugins_vK0DVI { "/": "/tmp/htm ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 673ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/7] gen bake.{client,server,error}.js -> bake.client.js, bake.server.js, bake.error.js [2/7] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited [2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/runtime/bake/DevServer.rs | 63 +++- src/runtime/bake/dev_server/lifecycle.rs | 12 +- src/runtime/bake/dev_server/mod.rs | 401 +++++++++++---------- src/runtime/dispatch.rs | 10 +- .../http/bun-serve-html-hot-reload-drop.test.ts | 82 +++++ test/js/bun/http/bun-serve-html.test.ts | 10 +- 6 files changed, 374 insertions(+), 204 deletions(-) ``` </details> **gate history** · 3 passed · 2 rejected · iteration 4 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/runtime/bake/DevServer.rs 10 13 0 src/runtime/bake/dev_server/lifecycle.rs 5 9 0 src/runtime/bake/dev_server/mod.rs 13 12 0 src/runtime/dispatch.rs 3 2 0 test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts 1 5 0 test/js/bun/http/bun-serve-html.test.ts 6 9 0 ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
pull Bot
pushed a commit
that referenced
this pull request
Jul 29, 2026
…rier (oven-sh#36337) `JSNativeStreamSourceAdapter::m_controller` was a `JSC::Weak<JSReadableStreamDefaultController>`. When the native pull promise is rejected (socket fault on a fetch body) the adapter is queued as the `onNativePullRejected` reaction context, which roots the **adapter** but not the **controller**: the adapter's only edge to it was the `Weak`. `FetchTasklet` releases both native `Strong<>`s to the body stream before that microtask drains, so a GC in between can leave the entire consumer graph (`controller -> stream -> reader -> pipe op -> destination -> writer -> readyPromise`) white. The subsequent error cascade then enqueues the pipe's writes-drained shutdown deferral against a corpse `op`, and `performPipeShutdownAction(AbortDestination)` dereferences a swept `readyPromise`: ``` ASSERTION FAILED: result JSObject.h(583) JSGlobalObject *JSC::JSObject::realm() const #5 JSC::JSObject::realm() #6 JSC::JSPromise::rejectPromise #7 JSC::JSPromise::reject #8 Bun::WebStreams::writableStreamDefaultWriterEnsureReadyPromiseRejected #9 Bun::WebStreams::writableStreamStartErroring #10 Bun::WebStreams::writableStreamAbort #11 WebCore::performPipeShutdownAction (AbortDestination) #12 WebCore::JSStreamPipeToOperation::onWritesFinishedForShutdown ``` On builds without the assert the same path is a silent write into freed/reused promise memory. ## Fix Hold `m_controller` as a visited internal field so a queued adapter roots the controller directly. The edge is cleared on every terminal path (`nativeSourcePullRejected`, `nativeSourceCallClose`, `nativeSourceCancel`); `controller->algorithmContext` is cleared by `readableStreamDefaultControllerClearAlgorithms`, so the abandoned case is an ordinary intra-heap cycle mark-sweep collects. `NewSource::this_jsvalue` is only `Strong` during FileReader I/O, where pinning the consumer graph is the correct behavior anyway. With the `Weak` gone the adapter no longer needs a destructor, so it is now a `JSInternalFieldObjectImpl<5>`: the five JSValue members (handle, pendingView, closer, drainValue, controller) are internal fields visited by the base class, with typed accessors at call sites. The scalar members (chunkSize, flag bitfield, text-decode state) stay as plain members. ## Verification `native-source-onclose-leak.test.ts` (the partial-read + `releaseLock` abandonment tests for Blob/fetch/File sources) continues to pass, confirming the cycle does not pin. `streams.test.js`, `pipeTo-signal-leak.test.ts`, `compression.test.ts`, `blob.test.ts` all pass. The crash itself is 0/1800 standalone; it reproduces ~1/3 only under a fault-injected tracer replay. `pipeTo-shutdown-gc.test.ts` exercises the shape (native body source, socket fault mid-stream, fire-and-forget `pipeTo` under `collectContinuously`, `AbortDestination` shutdown arm) as a regression surface. <!-- robobun:evidence:begin --> --- **no test proof** · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/streams/pipeTo-shutdown-gc.test.ts <!-- robobun:evidence:end -->
pull Bot
pushed a commit
that referenced
this pull request
Aug 2, 2026
…during VM shutdown (oven-sh#36750) Fixes `test/js/bun/http/bun-serve-html-405.test.ts` going red on x64-asan (build [87498](https://buildkite.com/bun/bun/builds/87498) and several unrelated PR builds since ~87000). ## Repro ``` BUN_DESTRUCT_VM_ON_EXIT=1 ASAN_OPTIONS=detect_leaks=1 \ LSAN_OPTIONS=suppressions=test/leaksan.supp \ bun-debug test test/js/bun/http/bun-serve-html-405.test.ts ``` ``` Indirect leak of 2104 byte(s) in 1 object(s) allocated from: ... #11 new<bun_runtime::server::NewServer<false, true>> #12 init<false, true> src/runtime/server/mod.rs:2009:47 #13 bun_runtime::api::bun_object::serve src/runtime/api/BunObject.rs:1564:26 ... #21 BunObject_callback_serve src/runtime/api/BunObject.rs:230:25 SUMMARY: AddressSanitizer: 2942 byte(s) leaked in 7 allocation(s). ``` 10/10 without this change, 0/10 with it (local debug+ASAN). ## Cause `using server = Bun.serve({ development: true, routes: { "/": html } })` disposes via `stop(true)`, which makes `deinit_if_we_can()` downgrade `js_value` to `Weak` and return. The `NewServer` Box is only freed once the JS wrapper's `finalize()` fires and `schedule_deinit()` enqueues the actual `deinit()` as a `ManagedTask`. When the wrapper survives to `lastChanceToFinalize` (`BUN_DESTRUCT_VM_ON_EXIT=1`, which the CI runner sets on ASAN lanes), `global_exit()` has already had its last event-loop tick. The enqueued task never runs, and `EventLoop::deinit()` drops the task box without a cleanup (`ManagedTask::new` sets `cleanup: None`). The 2104-byte `NewServer<false, true>` Box, its `config.static_routes` Vec, the `html_bundle::Route` it refcounts, and the route's path strings are all orphaned. `Route.server: Cell<Option<AnyServer>>` points back at the server so LSan sees a pointer cycle and reports every allocation as indirect. The path has always existed, but before oven-sh#35356 the per-tick GC sampler usually collected the wrapper during the handful of event-loop ticks between the test body and `global_exit()`, so `schedule_deinit()` ran while the loop was still live. With only the 1s idle-timer GC, a single fast test like this one reaches shutdown with the wrapper still alive more often (about half the PR builds since the merge). ## Fix `schedule_deinit()` now sets `DEINIT_SCHEDULED` and returns without enqueueing when `is_shutting_down()`. `finalize()` then frees the Box synchronously when the server has been fully drained: it unboxes via `Box::into_raw` first so the dealloc goes through the raw owning pointer rather than a `&mut self` frame (whose FnEntry protector would make the dealloc Stacked-Borrows UB, same pattern as `Listener::finalize` / `UDPSocket::finalize`). Every JSC handle on the Drop chain (`JSPromiseStrong`, `JsRef`, `UserRouteBuilder.callback: Strong`) funnels through `Strong::Impl::destroy`, which is a no-op past `is_shutting_down()`, so freeing here is safe. The inline free is gated on `TERMINATED`: `NewApp::destroy` runs `us_socket_group_deinit`, which unlinks the socket group from the loop's list without closing any sockets still in it. A graceful `stop()` only closes the listener and leaves keep-alive sockets open in the group; destroying the app there would orphan them (seen as a 280-byte `us_poll_t` direct leak on `vendor/elysia/test/core/before-handle-arrow.test.ts` with an earlier revision of this PR, and a `US_ASSERT(head_sockets==NULL)` abort on the debug build). `TERMINATED` is set only once `app.close()` has run, so the inline free is taken for abruptly-stopped servers (what `using server` does) and skipped for gracefully-stopped ones, which is identical to `main`'s behaviour for them. Other callers that can reach `schedule_deinit()` past shutdown (a last request draining inside `close_all_socket_groups`, which runs before `lastChanceToFinalize`) only set the flag and leave the Box, since `NewApp::destroy` there would delete the uws socket group mid-iteration. ## Verification Two ASAN-only subprocess tests added: - abrupt `stop(true)` of a dev server with an HTML route under `BUN_DESTRUCT_VM_ON_EXIT=1` + `detect_leaks=1`: fails on `main` with the 7-allocation LSan report above; passes with this change. - graceful `stop()` of a plain server with a keep-alive client connection: passes on both `main` and this change (asserts `us_socket_group_deinit`'s `head_sockets==NULL` precondition, which an earlier revision of this change violated). <!-- robobun:evidence:begin --> --- **no test proof** · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/http/bun-serve-html-405.test.ts <!-- robobun:evidence:end -->
pull Bot
pushed a commit
that referenced
this pull request
Aug 2, 2026
) ## What `JSSink::assign_to_stream` now detaches the freshly created `JSReadable*SinkController` (nulling its `m_sinkPtr`) when the C++ stream-pump setup returns an error, before returning to the caller. ## Why The generated `${name}__assignToStream` functions create the controller with `m_sinkPtr = sinkPtr` and then call into `GlobalObject::assignToStream` → `readDirectStream` / `readStreamIntoSink`. If that setup throws (for example a direct `ReadableStream` whose `pull` getter throws), the controller is never started, so nothing ever calls `end()`/`close()` to null `m_sinkPtr`. The caller's error path (`Writable::init` for `Bun.spawn`) then releases and frees the native sink. When the controller is later swept, its destructor runs `${name}__controllerDetached` / `${name}__finalize` on freed memory. ASAN report: ``` heap-use-after-free on address 0x799feed81c78 READ of size 1 #0 JSSink<FileSink>::js_controller_detached Sink.rs:567 #1 FileSink__controllerDetached generated_jssink.rs:179 #2 JSReadableFileSinkController::~JSReadableFileSinkController() freed by: #12 FileSink::deinit FileSink.rs:1142 #16 Writable::pipe_release Writable.rs:70 #17 Writable::init Writable.rs:339 #18 spawn_maybe_sync js_bun_spawn_bindings.rs:1379 ``` The fix is at the generic `JSSink::assign_to_stream` layer so it covers every sink type (`FileSink`, `NetworkSink`, `FetchRequestBodySink`, ...), not just the spawn path. ## Repro ```js const { openSync, closeSync } = require("node:fs"); const fd = openSync("/tmp/out.txt", "w"); let armed = false; const stream = new ReadableStream({ type: "direct", get pull() { if (armed) throw new Error("pull unavailable"); return () => {}; }, }); armed = true; try { Bun.spawn({ cmd: [process.execPath, "-e", "0"], stdio: [stream, fd, "ignore"] }); } catch {} closeSync(fd); Bun.gc(true); // sweep -> controller dtor -> UAF ``` ## Tests The two existing `spawn.test.ts` cases that cover the stdin-stream-setup-throws path now force a full GC in the child fixture so the controller destructor runs deterministically under debug+ASAN as well. Previously they were only failing on the release-asan lane (where the whole file has been quarantined as `[ASAN] [TIMEOUT]`), which is why this went unnoticed. ``` bun bd test test/js/bun/spawn/spawn.test.ts -t "stdin stream setup fails" ``` fails on `main` (ASAN heap-use-after-free in the child's stderr) and passes with this change. `spawn-stdin-readable-stream-edge-cases.test.ts` and `body-stream.test.ts` continue to pass.
pull Bot
pushed a commit
that referenced
this pull request
Aug 5, 2026
… sink ends inline (oven-sh#36939) ### Crash Sentry [BUN-3BZF](https://bun-p9.sentry.io/issues/?query=BUN-3BZF) (2,975 events since 2026-05-25, macOS-dominant): `Panic: called Option::unwrap() on a None value` at `FetchTasklet::callback`'s `task_ref.http.as_mut().unwrap()`, reached from the HTTP thread's result dispatch (`us_internal_ssl_on_data -> HTTPClient::fail -> dispatch_result_and_reset -> AsyncHTTP::on_async_http_callback_raw -> FetchTasklet::callback`). `http` is set once at creation and cleared only at deinit, so the panic means the callback ran against a freed `FetchTasklet`. ### Cause `start_request_stream` takes a `+1` on the tasklet that must be released exactly once by `write_end_request`. For a native `ByteStream` request body (an upstream response body piped into `fetch()`), `wire_native_sink` installs the sink's `source` handle *before* any of its `EndedInline` returns (`ReadableStream.rs:328` vs `:337/:352/:359`), so a stream that picked up an error or its last chunk between `fetch()` and the `can_stream` tick comes back `EndedInline` with a native source attached. The `EndedInline` arm released the `+1` (via `write_end_request`) but left `self.sink` installed with `ended == false`. Every terminal path then runs `cancel_request_body_sink`, which saw a "live" native sink and took its native arm: `abort_task()` plus a second `write_end_request` — releasing the same `+1` again. The double release collapses the refcount while the other owners (the JS-side initial ref and the HTTP thread's in-flight ref) still use the tasklet. Under ASAN the deterministic form is the trace below (deinit runs inside `cancel_request_body_sink`, then `on_progress_update` keeps using `self`). In release builds the same imbalance frees the tasklet while it is still in use (or double-frees, handing a live tasklet's block back to the allocator), which surfaces as downstream crashes in the fetch completion path — the BUN-3BZF unwrap is the tasklet's `http` field read from freed/recycled memory. ``` READ of size 8 ... core::mem::replace::<bun_jsc::js_promise::Strong> #2 FetchTasklet::on_progress_update FetchTasklet.rs:1158 freed by thread T0 here: #12 FetchTasklet::deinit FetchTasklet.rs:509 #16 FetchTasklet::write_end_request FetchTasklet.rs:2281 #17 FetchTasklet::cancel_request_body_sink FetchTasklet.rs:2368 #18 FetchTasklet::on_progress_update FetchTasklet.rs:1143 ``` ### Fix Leave the sink in the same state `end_from_stream` (the normal native termination) leaves it: `ended = true`, source and task detached. The terminal `cancel_request_body_sink` then hits its existing `if sink.ended { return }` guard and cannot release the ref a second time (it also no longer spuriously aborts a request whose body simply ended inline). ### Verification - New fixture `fetch-stream-body-ended-inline-fixture.ts` drives the window: an upstream server that advertises a larger `content-length` than it sends and closes a few ms later, piped as the body of a TLS `fetch()` (the handshake keeps the wire-attempt window open), 100 iterations. - Unfixed debug+ASAN build: heap-use-after-free with the trace above, 8/8 runs. - Fixed build: `bun bd test test/js/web/fetch/fetch-abort-stream-body.test.ts` passes (5 pass, 1 pre-existing skip), including the new test. - `test/js/web/fetch/body-stream.test.ts`: 9086 pass / 0 fail. `fetch.test.ts` and `fetch.stream.test.ts`: identical pass/fail counts to an unfixed baseline in the same container (the failures are pre-existing network/timeout issues). - The test is `skipIf(!isASAN)`: the release build corrupts silently, so only sanitizer lanes can observe the failure.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.1)
Can you help keep this open source service alive? 💖 Please sponsor : )