Skip to content

docs: state that Poly is derived from Bun - #5

Merged
liooil merged 1 commit into
mainfrom
claude/readme-derived-from-bun
Aug 7, 2026
Merged

docs: state that Poly is derived from Bun#5
liooil merged 1 commit into
mainfrom
claude/readme-derived-from-bun

Conversation

@liooil

@liooil liooil commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Add a 'Derived from Bun' section to the top of README.md (and the Chinese mirror): Poly is a fork of Bun (originally oven-sh/bun); the Bun source tree and history are part of this repository, with a Rust host and embedded RustPython layered on top. Record the fork origin commit and the upstream tracking policy, and link the build section to the new section instead of duplicating the origin commit hash.

What does this PR do?

How did you verify your code works?

Add a 'Derived from Bun' section to the top of README.md (and the Chinese
mirror): Poly is a fork of Bun (originally oven-sh/bun); the Bun source tree
and history are part of this repository, with a Rust host and embedded
RustPython layered on top. Record the fork origin commit and the upstream
tracking policy, and link the build section to the new section instead of
duplicating the origin commit hash.
@liooil
liooil merged commit c89df6a into main Aug 7, 2026
3 of 4 checks passed
liooil pushed a commit that referenced this pull request Aug 7, 2026
…e cache (#37034)

### Problem

On the `13 x64-asan` lane, a test that exercises non-ISO Temporal
calendars from a test callback can abort after a fully green run with a
LeakSanitizer report. Seen in build 89504 on #37024, whose
`test/js/bun/bun-object/deep-equals-temporal.test.ts` uses
`[u-ca=hebrew]`:

```
Direct leak of 624 byte(s) in 1 object(s) allocated from:
    #1 icu_75::HebrewCalendar::clone() const
    #2 icu_75::Calendar::createInstance(icu_75::TimeZone*, icu_75::Locale const&, UErrorCode&)
    #3 ucal_open_75
    #4 JSC::TemporalCore::buildCalendarTemplate(WTF::AbstractLocker const&, unsigned int)
    #5 JSC::TemporalCore::withCalendar<JSC::TemporalCore::calendarYear(...)::$_0>(...)
```

The CI annotation titles this `direct leak of 624b in {closure#0}
(src/jsc/JSValue.rs:1664:22)` because that is the first in-repo frame
(the test-runner's `JSValue::call`); everything below it is WebKit/ICU.

### Cause

`TemporalCore::withCalendar`
(`vendor/WebKit/.../temporal/core/CalendarICUBridge.cpp`) keeps up to 8
open `UCalendar` templates in a process-lifetime `LazyNeverDestroyed`
`TinyLRUCache`, one per calendar ID (non-ISO arithmetic, plus pure-ISO
`PlainDateTime.prototype.with`, which reaches the same path unguarded);
LRU eviction `ucal_close`s them, so the set is bounded. The
`CalendarCacheEntry` that owns each `UCalendar` is
`WTF_MAKE_TZONE_ALLOCATED` (bmalloc), which LSan does not scan, so the
libc-allocated `UCalendar` (and the ICU `TimeZone` inside it) is
reported as a direct leak even though it is reachable. Whether a given
run aborts depends on whether some stale stack or register value still
points at the ICU object when LSan scans at exit, hence the
intermittence.

This is the calendar twin of the already-suppressed
`TemporalCore::withTimeZone` entry (same cache design, same
TZone-allocated owner).

### Fix

- Add a `leak:TemporalCore::buildCalendarTemplate` suppression to
`test/leaksan.supp`, mirroring the `withTimeZone` entry. The pattern
anchors on the template builder rather than `withCalendar` itself so
that a future real leak inside one of the many op lambdas `withCalendar`
runs would still be reported; every cached-template allocation carries
the builder frame. (`withTimeZone` has no such builder frame, its
`ucal_open` is inline, so that entry keeps its existing pattern.)
- Drop the `test/no-validate-leaksan.txt` escape hatch #37024 added for
`deep-equals-temporal.test.ts`, re-enabling leak validation for it; that
file exercises the suppressed path on the asan lane.

### Verification

On a debug ASAN build, running `bun test
test/js/bun/bun-object/deep-equals-temporal.test.ts` under the CI
leak-validation env (`BUN_DESTRUCT_VM_ON_EXIT=1`,
`detect_leaks=1:abort_on_error=1`, repo suppression file):

- with the new entry: clean exit, 5/5 runs
- without it: LSan abort with the calendar-template stacks above, 3/3
runs

A standalone probe exercising 8 non-ISO calendars plus pure-ISO
`PlainDateTime.with` from a timer callback shows the same split (10/10
aborts without, 10/10 clean with; `print_suppressions=1` attributes
exactly the ICU template allocations to the new entry). Top-level module
code cannot reproduce this: its allocation stacks carry
`JSC::JSModuleLoader::evaluateNonVirtual`, which the suppression file
already covers wholesale. An ASAN-gated test pinning the entry was part
of an earlier revision and was dropped per review; the re-enabled
`deep-equals-temporal.test.ts` covers the path in CI instead.

The Expect-wrapper shutdown leak mentioned in the dropped no-validate
comment is a separate issue tracked in #32180: that is `bun test`'s own
finalizer-owned memory, while this cache deliberately survives VM
teardown, so #32180 would not prevent this report.

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 1 · docs-only change; test-proof not
applicable

<!-- robobun:evidence:end -->

---------

Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
<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
liooil pushed a commit that referenced this pull request Aug 7, 2026
…Worker" (#21994)

Reverts oven-sh/bun#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
```
liooil pushed a commit that referenced this pull request Aug 7, 2026
Fixes a segfault when reading `.fd` on the result of `Bun.listen({ tls:
{ ... } })`.

`Listener.getFD` was calling `uws_listener.socket(true).fd()` for TLS
listeners. For `is_ssl=true`, the uSockets wrapper
`us_internal_ssl_socket_get_native_handle` returns `s->ssl`, and `fd()`
then calls `SSL_get_fd()` on it. But a listen socket has no SSL object —
SSL is per-connection — so `s->ssl` is uninitialized memory (ASAN poison
`0xbebebe...`) and the call segfaults.

Listen sockets always have a plain poll fd regardless of TLS, so get it
via the non-SSL path.

```
#3 SSL_get_rfd (ssl=0xbebebe0000000018)
#4 SSL_get_fd  (ssl=0xbebebe0000000018)
#5 deps.uws.socket.NewSocketHandler(true).fd () at src/deps/uws/socket.zig:283
   bun.js.api.bun.socket.Listener.getFD at src/bun.js/api/bun/socket/Listener.zig:532
```

Repro (also triggered when `console.log()` introspects the listener):

```js
const s = Bun.listen({
  hostname: "localhost", port: 0,
  socket: { data(){}, open(){}, close(){} },
  tls: { passphrase: "abc" },
});
console.log(s.fd);
```

Found by Fuzzilli.

---------

Co-authored-by: robobun <robobun@users.noreply.github.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…9910)

## What

`Blob.dupeWithContentType` guarded its content_type handling on
`duped.isHeapAllocated()` immediately *after* calling
`duped.setNotHeapAllocated()`, so both branches were dead. When the
source Blob's `content_type` is heap-allocated, the bitwise-copied dupe
aliased the same allocation while both sides had `content_type_allocated
== true`.

This is a regression from #23015: the pre-refactor code checked
`duped.allocator != null` *before* clearing it at the end of the
function; the refactor moved the clear to the top but left the (renamed)
guard in place.

## Repro

```js
const file = Bun.file(path, { type: "application/x-custom-type-not-in-registry-abcdefghijklm" });
const response = new Response(file);               // body holds a dupe that aliases file.content_type
await file.write("hello", { type: "application/x-..." }); // frees file.content_type
response.headers.get("content-type");              // reads freed memory
```

On ASAN builds:
```
==716==ERROR: AddressSanitizer: use-after-poison on address 0x71df454301c0
    #1 in Zig::toStringCopy(ZigString) helpers.h:217
    #2 in WebCore__FetchHeaders__put bindings.cpp:2082
    #5 in bun.js.webcore.Response.getOrCreateHeaders Response.zig:358
```

On release builds the freed slot gets reused and the read produces
garbage:
```
TypeError: Header '25' has invalid value: 'ion/x-custom-type-not-in-registry-abcdefghijklm'
```

## Fix

Drop the `isHeapAllocated()` guard and always deep-copy an allocated
`content_type` in `dupeWithContentType`. The old `!include_content_type`
branch's "resolve to static mime or fall back to empty" is gone — it
would have dropped FormData's `multipart/form-data; boundary=...` (and
any non-registry type) on `Response.clone()`, and the branch itself was
marked `// TODO: fix this / this is a bug`. The `include_content_type`
parameter is now a no-op.

Since every dupe now owns its `content_type` copy, `Blob.deinit()` frees
it. That in turn required closing a few places that held a
bitwise-copied Blob alongside the live owner:

- `fromJSWithoutDeferGC` `move=true`: deep-copy `name`/`content_type`
into the moved-out value so the source JS Blob keeps sole ownership; the
BuildArtifact arm now `dupe()`s (its "move" only nulled the store on a
local copy).
- `getSliceFrom()`: free the dupe's copy before overwriting it with the
slice's own type.
- `doWrite`/`getWriter`: clear `content_type_allocated` after the
in-place free so a registry-resolved static string isn't later freed by
`deinit()`.
- `BlobOrStringOrBuffer.deinitAndUnprotect`: only deref the store
(matching its `deinit()`) since `.blob` is a raw view of a live JS Blob.

## Verified

- `bun bd test test/js/web/fetch/blob.test.ts` — 16/16 pass
- New UAF test fails on both debug/ASAN (use-after-poison) and system
bun (garbage header) without the fix, passes with it
- New clone test guards against dropping FormData's boundary on
`Response.clone()`
- ASAN stress: 1k×
`Response.clone`/`blob.slice`/`createObjectURL`+revoke/`new
Response([blob])`/`write({type})` — no double-free
- RSS is flat across 50k × 1KB-type `Response.clone()` and
`blob.slice()`

---------

Co-authored-by: robobun <robobun@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…es (#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>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…#30136)

## Repro

```js
const client = await Bun.connect({ hostname, port, tls, socket: { ... } });
// after handshake:
client.end("x");
client.flush();   // ← second markInactive frees *Handlers
// peer replies close_notify → onClose derefs freed Handlers
```

ASAN on debug build:

```
==4075==ERROR: AddressSanitizer: use-after-poison on address 0x7aff355e0469
READ of size 1 at 0x7aff355e0469 thread T0
    #0 bun.js.api.bun.socket.NewSocket(true).onClose  src/bun.js/api/bun/socket.zig:661:46
    #1 deps.uws.handlers.PtrHandler(...).onClose      src/deps/uws/handlers.zig:49:61
    ...
    #5 us_internal_ssl_on_close                       packages/bun-usockets/src/crypto/openssl.c:940:29
```

## Cause

`end()` → `internalFlush` → `canEndAfterFlush()` → `markInactive()` →
`closeAndDetach(.normal)` detaches `this.socket` and calls
`us_socket_close(code=0)`. For TLS with `code==0`,
`us_internal_ssl_close` sends close_notify and **defers** the raw close
until the peer replies (so the loop stays alive to receive it).
`markInactive` returns early without clearing `is_active`, relying on
the eventual `onClose` → `markInactive` to run `handlers.markInactive()`
and free the client-mode `*Handlers`.

`flush()` was the only `internalFlush()` caller without an
`isDetached()` guard. Calling it in that window re-enters
`canEndAfterFlush()` (still `is_active && end_after_flush`) →
`markInactive()`, which now sees the detached socket as closed and runs
the **full** teardown: `handlers.markInactive()` → `active_connections
== 0` → `vm.allocator.destroy(handlers)`. When the peer's close_notify
later arrives, `onClose` calls `this.getHandlers()` on freed memory.

## Fix

Add the same `isDetached()` early-return to `flush()` that `end()`,
`endBuffered()`, `onWritable`, and every other `internalFlush()` caller
already have.

## Verification

New test in `test/js/bun/net/socket.test.ts` spawns a TLS client that
does `end("x"); flush(); flush();` after handshake and awaits `close`.

- Without fix (`git stash -- src/`): subprocess aborts with the ASAN
trace above; test fails.
- With fix: subprocess prints `OK` and exits 0; test passes.

Co-authored-by: robobun <robobun@users.noreply.github.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…before write (#30155)

## Repro

```js
Bun.serve({
  port: 0,
  fetch: () =>
    new Response("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:

```
AddressSanitizer: heap-use-after-free
READ of size 13
  #2 uWS::HttpResponse<false>::writeHeader
  #5 doRenderHeadResponse  RequestContext.zig:1378
freed by:
  #23 HTTPHeaderMap::remove
  #28 doWriteHeaders       RequestContext.zig:2303
  #29 renderMetadata       RequestContext.zig:2209
  #30 doRenderHeadResponse RequestContext.zig:1377
```

## Cause

`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 *before* `renderMetadata()` (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.

Co-authored-by: robobun <robobun@users.noreply.github.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…er (#31333)

### Problem

Fuzzing found a second transpiler stack overflow
(`sig:SIGSEGV:nostack`): ~600 nested `{` blocks crash the process.

```js
new Bun.Transpiler({ loader: "tsx", target: "bun", minifyWhitespace: true, deadCodeElimination: true })
  .transformSync("{".repeat(600) + 'class Test1 { static "prop1" = 0; }' + "}".repeat(600));
```

#31242 guarded the **expression** recursion (`visit_expr_in_out`,
`print_expr`, DCE helpers), but the **statement** recursion was left
unguarded. Nested blocks stay under `MAX_STMT_DEPTH` (1000) in
`parse_stmt`, then the visit pass recurses through `visit_stmts →
visit_and_append_stmt → s_block → visit_stmts` with no stack check —
each level stacks several multi-KB frames, so a few hundred levels
exhaust the thread's stack (reproduces at depth 800 on a debug build's 8
MB main stack; smaller stacks crash at 600):

```
#5  visit_stmts                 src/js_parser/visit/mod.rs:1280
#6  s_block                     src/js_parser/visit/visit_stmt.rs:1627
#7  visit_and_append_stmt       src/js_parser/visit/visit_stmt.rs:108
#8  visit_stmts                 src/js_parser/visit/mod.rs:1336
... (repeats until SIGSEGV)
```

### Fix

Guard the statement recursion the same way the expression recursion
already is:

- `visit_and_append_stmt` now checks `stack_check.is_safe_to_recurse()`
(plus the `reported_stack_overflow` fast-path) and reports "Maximum call
stack size exceeded" instead of descending, mirroring
`visit_expr_in_out`.
- `print_stmt` and `print_if` (which self-recurses for `else if` chains
without passing through `print_stmt`) get the same guard
`print_expr`/`print_binding` already have, so a deep AST printed on a
thread with less stack headroom errors instead of overflowing.
- Removed the `MAX_STMT_DEPTH`/`parse_stmt_depth` hard cap from
`parse_stmt` (review feedback): recursion depth in every phase is now
governed by `StackCheck` alone, matching the Zig parser.
- Guarded `hoist_symbols` the same way: it walks the scope tree before
the visit pass at the full depth the parser allowed, and was only kept
safe previously by the now-removed cap (the 15k-deep
`lots-of-for-loop.js` fixture overflowed it in release builds
otherwise).

With this, every arbitrarily-nestable AST recursion (statements,
expressions, bindings) is stack-checked in all three phases (parse,
visit, print); deep inputs throw a catchable `Maximum call stack size
exceeded` error.

### Verification

New test `deeply nested statement blocks error instead of crashing the
process` in `test/bundler/transpiler/transpiler.test.js` transpiles
nested-block and `else if`-chain shapes at depths 600/800/990 (below the
parse-time cap, deep enough to overflow an unguarded visitor) in a
subprocess and asserts it exits cleanly.

- Without the fix: the subprocess dies with SIGSEGV at depth 800+ (debug
build), so the test fails.
- With the fix: `bun bd test test/bundler/transpiler/transpiler.test.js`
→ 147 pass, 0 fail; the repro above now throws `Maximum call stack size
exceeded`.
liooil pushed a commit that referenced this pull request Aug 7, 2026
…letes mid-read (#31959) [publish images]

Fixes a use-after-free in the HTTP client's proxy tunnel close path
(Sentry BUN-2VY8, ~10 events/day on Windows release builds; reproduces
deterministically under ASAN on all platforms).

## Repro

`fetch()` through an HTTP CONNECT proxy to an HTTPS origin, where the
origin's final response bytes and its TLS `close_notify` reach the
client in a single TCP batch (origin writes the response and immediately
closes). The regression test builds exactly that: a local CONNECT proxy
that holds origin-to-client bytes after the handshake and flushes
session tickets + response + close_notify in one write.

On an unfixed ASAN build:

```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 8 thread T11 (HTTP Client)
  #0 Option<RefPtr<ProxyTunnel>>::as_ref
  #1 bun_http::proxy_tunnel::on_close                    src/http/ProxyTunnel.rs:525
  #2 SSLWrapper<*mut HTTPClient>::trigger_close_callback src/uws/lib.rs:802
  #3 SSLWrapper<*mut HTTPClient>::handle_reading         src/uws/lib.rs:1022
  #4 SSLWrapper<*mut HTTPClient>::handle_traffic
  #5 SSLWrapper<*mut HTTPClient>::receive_data
  #6 ProxyTunnel::receive                                src/http/ProxyTunnel.rs:751
freed by:
  AsyncHTTP::on_async_http_callback_raw                  src/http/AsyncHTTP.rs:813
  HTTPClient::send_progress_update_without_stage_check   src/http/lib.rs:3793
```

## Cause

1. `handle_reading` processes the batch: `SSL_read` returns the body
bytes, the next `SSL_read` hits `close_notify`
(`SSL_ERROR_ZERO_RETURN`), which sets `received_ssl_shutdown` and
`sent_ssl_shutdown` before flushing the already-decrypted bytes through
the data callback.
2. The data callback completes the response. The done path runs
`close_proxy_tunnel(true)` -> `ProxyTunnel::shutdown()` ->
`SSLWrapper::shutdown(true)`, which hits the already-shut-down early
return (`sent_ssl_shutdown || fatal_error`) and returns **without
setting `closed_notified`**. The result callback then frees the
`ThreadlocalAsyncHTTP` embedding the `HTTPClient`, the exact pointer
stored in the wrapper's `handlers.ctx`.
3. Control returns to `handle_reading`. Its liveness guard
(`ssl.is_none() || closed_notified()`) passes because neither is set, so
`trigger_close_callback()` invokes `on_close(handlers.ctx)` on the freed
client. When the allocation has been recycled, `on_close` can ref or
close a different request's tunnel instead of faulting.

## Fix

`src/uws/lib.rs`: when `SSLWrapper::shutdown(fast_shutdown=true)` takes
the already-shut-down early return, fire `trigger_close_callback()`
(idempotent via `closed_notified`) so the wrapper is marked closed
before the owner detaches and frees `handlers.ctx`. A fast shutdown is a
full teardown, and the normal fast-shutdown path already fires the close
callback unconditionally; this only closes the gap where the SSL-level
shutdown had already happened.

Graceful `shutdown(false)` (node:tls half-close via UpgradedDuplex /
WindowsNamedPipe) is unchanged, so reads after a sent `close_notify`
keep working.

## Verification

New test in `test/js/bun/http/proxy.test.ts` (`test.skipIf(!isASAN)`,
the UAF is only deterministic under ASAN): fails on an unfixed ASAN
debug build with the heap-use-after-free above, passes with the fix.
Full `proxy.test.ts` (46 tests) plus `node-tls-connect`,
`node-tls-upgrade`, `node-tls-duplex-close-throw-uaf`,
`node-tls-socket-allow-half-open-option`, `node-tls-server`,
`fetch-tls-cert`, and `node-https-checkServerIdentity` suites pass.

## Note on the asan-lane CI failure (#32144)

The intermittent LeakSanitizer failure on the x64-asan shard (deferred
napi finalizers parked on a never-drained cleanup-hook list at `bun
test` exit) is being fixed in #32146, which carries the same
`global_exit()` drain plus a hooks-only guard that skips pending
`napi_wrap` finalizers on undrained-loop exits. A subset version of that
fix was briefly on this branch (e59bc1d0) but without the hooks-only
guard it made `test/js/third_party/duckdb/duckdb-basic-usage.test.ts`
SEGV at exit on the asan lane (build 62135), exactly the failure mode
#32146's guard prevents, so it was reverted (61f9e701). This PR is
scoped to the proxy-tunnel UAF; its asan lane can still intermittently
hit the pre-existing #32144 leak until #32146 lands.

## Related PRs

- #30606 addresses the same crash signature but patches only the `.zig`
reference files, which are no longer compiled; this PR fixes the
shipping Rust implementation.
- #31952 fixes the same UAF by calling a new `mark_close_notified()`
helper from `ProxyTunnel::shutdown` (silently setting the flag at one
call site, with `close_raw` exempted). This PR instead closes the gap
inside `SSLWrapper::shutdown(true)` itself, so every fast-shutdown
caller (`ProxyTunnel::shutdown`, `ProxyTunnel::close_raw`,
`UpgradedDuplex::close`, `WebSocketProxyTunnel::shutdown`) gets the same
"no callbacks after teardown" guarantee without new wrapper API or a
shutdown/close_raw asymmetry. The close callback is fired rather than
suppressed, so the error teardown path keeps delivering `on_close` ->
`close_and_fail` exactly once (idempotent via `closed_notified`). Test
here is a deterministic single-shot repro (the test proxy reassembles
TLS records and flushes tickets + response + close_notify in one write)
rather than an iteration loop.

---------

Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…sweep (#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>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…type (#32738)

### Repro

```js
using listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
await Bun.connect({
  hostname: "127.0.0.1",
  port: listener.port,
  socket: { open(s) { s.setTypeOfService({}); } },
});
```

On any assert build (debug, release-asan):

```
ASSERTION FAILED: isInt32()
  #4 JSC__JSValue__toInt32
  #5 TCPSocketPrototype__setTypeOfService
  #6 WebCore::TCPSocketPrototype__setTypeOfServiceCallback
```

On plain release the assert compiles out and the NaN-boxed bits of the
object handle are passed to `setsockopt(IP_TOS)` as the TOS byte.

### Cause

`set_type_of_service` in `src/runtime/socket/socket_body.rs` called
`args.ptr[0].to_int32()` on the raw argument. For non-numeric values
that falls through to the C++ `JSC__JSValue__toInt32`, which is
`JSC::JSValue::asInt32()` (the unchecked accessor that asserts
`isInt32()`), not a coercing conversion. The `node:net` wrapper
validates `tos` in JS before calling the handle, but the Bun-native
`Bun.connect` socket exposes this prototype method directly with no JS
validation layer.

### Fix

Route the argument through `validate_integer_range` with `min: 0, max:
255, field_name: "tos"`, the same pattern the sibling `setKeepAlive`
already uses for `initialDelay`. Non-numbers now throw
`ERR_INVALID_ARG_TYPE`, out-of-range integers throw `ERR_OUT_OF_RANGE`,
and non-integral numbers throw `ERR_INVALID_ARG_TYPE`, matching the
`node:net` surface.

I audited the other numeric setters on the TCPSocket/TLSSocket prototype
(`timeout`, `setMaxSendFragment`, `write` offset/length) and the
remaining `to_int32()` / `to_int64()` callers in `src/runtime/socket/`:
each is already gated by `is_number()` / `is_any_int()` or routes
through `coerce`. `setTypeOfService` was the only unguarded one.

### Verification

New test in `test/js/bun/net/socket.test.ts` spawns a subprocess that
calls `setTypeOfService` on a connected `Bun.connect` socket with `{}`,
`"x"`, `-1`, `256`, `1.5`, and `0x10`, and asserts the error code for
each plus that `getTypeOfService()` returns an integer. Without the fix
the subprocess aborts (exit 134) on the first call; with the fix all
seven checks pass. `test/js/node/test/parallel/test-net-socket-tos.js`
continues to pass.

Found by the bun-sys-fuzz API-grammar layer.
liooil pushed a commit that referenced this pull request Aug 7, 2026
… (#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.
liooil pushed a commit that referenced this pull request Aug 7, 2026
… (#32743)

## What

`ReadableStream::from_pipe` (the `proc.stdout` / `proc.stderr` path for
`Bun.spawn` and the shell subprocess) moves an already-registered pipe
poll from the subprocess `PipeReader` into a freshly allocated
`NewSource<FileReader>` and re-points the poll's owner at it. The
across-read ref that keeps that box alive (`waiting_for_on_reader_done`
+ `increment_count()`, which upgrades `this_jsvalue` to `Strong`) was
only taken in `FileReader::on_start`, i.e. the first time JS actually
pulls from the stream.

Between `from_pipe` and that first pull, the poll's owner points into a
box whose only ref is the JS wrapper's own `Weak` back-reference. If the
`Subprocess` and its cached stdout become unreachable before anyone
pulls (a fire-and-forget spawn where `proc.stdout` is touched but never
read, and the direct child exits while something else still holds the
write end), GC sweeps the `JSFileInternalReadableStreamSource` wrapper
and frees the `NewSource<FileReader>` box while the poll is still armed.
The next readability or EOF event dispatches into freed memory:

```
READ of size 8 (heap-use-after-free)
  #0 Vec::len / is_empty                                     (freed Vec<u8>)
  #2 webcore::file_reader::FileReader::on_reader_done        FileReader.rs:1008
  #3 bun_io::pipe_reader::read_socket{closure}               PipeReader.rs:846
  #4 PosixBufferedReader::read_socket                        PipeReader.rs:576
  #5 file-poll dispatch <- posix_event_loop <- us_internal_dispatch_ready_polls

freed by:   JSC::JSDestructibleObjectDestroyFunc <- MarkedBlock sweep <- MarkedSpace::sweepBlocks
allocated:  ReadableStream::from_pipe<subprocess::PipeReader> -> NewSource<FileReader>
```

Found by a coverage-guided GC-stress fuzzer with syscall interposition
(`BUN_JSC_collectContinuously=1` plus an injected `EAGAIN` to keep the
read pending). In release builds this is silent heap corruption.

## Fix

Take the across-read ref in `from_pipe` itself, immediately after the
live reader is transferred and the JS wrapper is created, so the box is
`Strong`-rooted for as long as the poll can fire. `on_reader_done` /
`on_reader_error` release it exactly as before. `FileReader::on_start`
now checks `waiting_for_on_reader_done` before taking the ref so the
later `handle.start()` call from `lazyLoadStream` does not double-count
on this path.

## How did you verify your code works?

The test asserts the lifetime invariant directly via
`heapStats().objectTypeCounts.FileInternalReadableStreamSource` rather
than racing for the crash, since the exact UAF trigger depends on the
fuzzer's syscall interposition. A detached grandchild (`sh -c 'while [ !
-e FLAG ]; do sleep 0.02; done; echo x'`) inherits the child's stdout
and keeps the write end open past the direct child's exit, so the
`FileReader`'s poll is still armed while we force GC with nothing in JS
referencing the wrapper.

- **Before** (`git stash push -- src/` + `bun bd test`): `duringLivePipe
= 0` of 4; every wrapper swept while its poll owner still points into
the freed box.
- **After**: `duringLivePipe >= 4`; once the grandchildren exit and the
pipes EOF, `afterEof <= 1` (one may remain via a conservatively-rooted
final `Subprocess`, same caveat as `spawn-ipc-gc.test.ts`).

Also passes `spawn-streaming-stdout.test.ts`,
`spawn-unread-stdout-gc.test.ts`, `spawn-ipc-gc.test.ts`,
`spawn-stdout-iterate-leak.test.ts`, and
`readablestream-helpers.test.ts`.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…ll-driven read (#32986)

## Problem

Heap use-after-free in Bun Shell when `epoll_ctl` fails while
re-registering a pipe's `FilePoll` from a poll-driven read. Found by
syscall-fault-injection fuzzing against `origin/main`. Follow-up to
#32754, which fixed the same failure on the eager spawn-time read path.

```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 1, thread T0
  #0 <bun_io::pipe_reader::BufferedReaderVTable>::link            io/PipeReader.rs:105
  #1 <bun_io::pipe_reader::BufferedReaderVTable>::on_read_chunk   io/PipeReader.rs:125
  #2 <bun_io::pipe_reader::PosixBufferedReader>::read_with_fn     io/PipeReader.rs:890
  #3 <bun_io::pipe_reader::PosixBufferedReader>::read_socket      io/PipeReader.rs:576
  #4 <bun_io::pipe_reader::PosixBufferedReader>::on_poll          io/PipeReader.rs:529
  #5 __bun_run_file_poll                                          runtime/dispatch.rs:677
freed by:
  <alloc::sync::Arc<bun_runtime::shell::subproc::PipeReader>>::drop
```

## Repro

1. `PipeReader::start` registers the poll and the eager spawn-time
`read_all()` hits `EAGAIN`, so `read_with_fn`'s `EAGAIN` arm
re-registers the poll and the spawn returns.
2. The child writes to stdout and the poll fires.
`__bun_run_file_poll`'s `BUFFERED_READER` arm dispatches straight into
`PosixBufferedReader::on_poll` with a bare `&mut *h` and no keepalive.
3. `read_with_fn` drains the chunk, `recv()` returns a real `EAGAIN`,
and `register_poll()` issues another `epoll_ctl`, which fails (`ENOMEM`
in the repro).
4. `register_poll` dispatches `on_reader_error`. The shell
`PipeReader::on_reader_error` signals the `Cmd`, the `Readable::Pipe`
`Arc` is dropped, and the callback's own `guard_from_raw` keepalive
becomes the last reference. The code already documents this: "Dropping
`guard` is the matching `deref()`; may free `this`."
5. Back in `read_with_fn`, the `EAGAIN` arm still delivers the drained
head: `parent.vtable.on_read_chunk(.., ReadState::Drained)` reads the
freed vtable.

Traced with the test's `LD_PRELOAD` shim:

```
[shim] epoll_ctl(ADD fd=13) unix call#1 -> ok       PipeReader::start
[shim] recv(fd=13)          unix call#1 -> EAGAIN   eager read, inside spawn
[shim] epoll_ctl(MOD fd=13) unix call#2 -> ok       re-register; spawn returns
[shim] recv(fd=13)          unix call#2             poll fired: the child's bytes
[shim] recv(fd=13)          unix call#3             real EAGAIN
[shim] epoll_ctl(MOD fd=13) unix call#3 -> ENOMEM   register_poll fails
[shell_subproc] PipeReader(0x..250) onReaderError errno: 12
[shell_subproc] PipeReader(0x..250, stdout) detach()
[shell_subproc] PipeReader(0x..250, stdout) deinit()
==ERROR: AddressSanitizer: heap-use-after-free
```

## Cause

`register_poll()`'s failure path dispatches `on_reader_error`, which the
`BufferedReaderParent` contract explicitly allows to free the parent,
but `register_poll` gave the caller no way to know that happened.
`read_with_fn`'s `EAGAIN` arm is the only call site that touches the
reader afterwards; every other `register_poll()` is in tail position.
The `SAFETY` comment above the `parent` rebind claimed the parent is
"never freed mid-call", which holds for `on_read_chunk` re-entry but not
for `on_reader_error`.

#32754 covered this exact sequence on the eager spawn-time entry by
holding an `Arc<PipeReader>` across `start()` and `read_all()` in
`Readable::start_pipe_reader`. The epoll dispatch has no equivalent
keepalive, so the poll-driven entry was still exposed.

## Fix

`PosixBufferedReader::register_poll()` now returns whether registration
succeeded. `false` means `on_reader_error` was dispatched and `self`
must not be touched again, so `read_with_fn`'s `EAGAIN` arm returns
there instead of delivering the drained head to a possibly freed parent.
The stream has already been completed with the registration error at
that point, so nothing is lost. All other `register_poll()` call sites
are tail calls and discard the result.

## Test

Two new modes in `test/js/bun/shell/shell-pipe-read-fault.test.ts`'s
`LD_PRELOAD` fault shim:

- `SHELL_RECV_EAGAIN_FIRST=1`: the first `recv()` on each `AF_UNIX`
socket returns `EAGAIN`, pushing the first successful read off the eager
spawn-time `read_all()` and onto the epoll dispatch.
- `SHELL_FAIL_EPOLL_FROM=N`: the Nth and later `epoll_ctl` `ADD`/`MOD`
on each `AF_UNIX` socket fail with `ENOMEM`. `N=3` lets the initial
registration and the eager read's re-registration succeed, then fails
the first poll-driven one.

The new test is `skipIf(!isASAN)` because the use-after-free is only
reliably observable under ASAN. With `src/io/PipeReader.rs` reverted to
`main` it fails in ~1.1s with the `heap-use-after-free` above; with the
fix all 6 tests in the file pass.

## Out of scope

Shell `PipeReader::on_read_chunk` also calls
`self.reader.register_poll()` from a `&mut self` method whose stated
contract is that it never frees `self`. If that inner registration
fails, the same free can happen under `read_with_fn`'s mid-loop flush
instead of its `EAGAIN` arm. Reaching it needs a large (>32 KB) burst in
one poll wake; I have not reproduced it, so it is not changed here.
liooil pushed a commit that referenced this pull request Aug 7, 2026
…ed (#33016)

A backend message that fails the connection can share a TCP read with
messages that follow it. `PostgresRequest::on_data`'s message loop had
no bail-out once `fail()` had run, so the trailing messages in that read
kept being dispatched against the already-failed connection.

### Repro

A mock backend that answers the StartupMessage with one write carrying
two messages:

```
R  int32(8) int32(99)   Authentication, unrecognized type
Z  int32(5) 'I'         ReadyForQuery
```

```ts
const sql = new SQL({ url: `postgres://u@127.0.0.1:${port}/db`, max: 1, idleTimeout: 1, connectionTimeout: 5 });
await sql`select 1`.catch(() => {});
await Bun.sleep(1600);
```

### Cause

The unrecognized `Authentication` type calls `fail()`, which sets the
status to `Failed`, closes the socket, and rejects the pending requests,
but the message loop keeps going and dispatches the `ReadyForQuery` from
the same read. That calls `set_status(Status::Connected)`, which has no
guard against leaving `Failed`, so the dead connection is flipped back
to `Connected` and the `on_data` epilogue re-arms its idle timer.
uSockets frees a closed `us_socket_t` at the end of the event-loop
iteration, so when the timer later fires, `ref_and_close` reads the
freed socket:

```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 1 at 0x71f2125605d2 thread T0
    #0 us_socket_is_closed                              packages/bun-usockets/src/socket.c:143:21
    #4 PostgresSQLConnection::ref_and_close             src/sql_jsc/postgres/PostgresSQLConnection.rs:1528:31
    #5 PostgresSQLConnection::fail_with_js_value        src/sql_jsc/postgres/PostgresSQLConnection.rs:726:14
    #6 PostgresSQLConnection::fail_fmt                  src/sql_jsc/postgres/PostgresSQLConnection.rs:749:14
    #7 PostgresSQLConnection::on_connection_timeout     src/sql_jsc/postgres/PostgresSQLConnection.rs:557:14
    #8 __bun_fire_timer                                 src/runtime/dispatch.rs:1020:35
0x71f2125605d2 is located 18 bytes inside of 104-byte region
freed by thread T0 here:
    #2 us_internal_free_closed_sockets                  packages/bun-usockets/src/loop.c:305:9
```

### Fix

- `PostgresRequest::on_data`: the message loop returns once the
connection's status is `Failed`. `fail()` is terminal; nothing after it
in the same read should be handled (a `DataRow`, `CommandComplete`, or
`ErrorResponse` in that position would be just as wrong as the
`ReadyForQuery`).
- `PostgresSQLConnection::set_status`: refuses to transition out of
`Failed`. The transition function owns that invariant; every other
consumer of `Status` (the timer interval, `update_has_pending_activity`,
the idempotency check in `fail_with_js_value`) already assumes `Failed`
is terminal.

### Verification

`test/js/sql/postgres-failed-connection-resurrection.test.ts` runs a
fixture against the mock backend above and lets it outlive the
idle-timer window. Without the fix the fixture dies with the ASan report
above; with it the fixture exits 0. Gated to ASan builds because the bug
is a read of freed memory, which release lanes do not detect.

The postgres fault-injection and integration suites still pass locally
(90 tests across `test/js/sql/postgres-*.test.ts`, `sql*.test.ts`,
`tls-sql.test.ts`).

### Related

- #32861 detaches the stored socket handle in `on_close` /
`on_connect_error` so nothing can dereference the freed `us_socket_t`
regardless of how the stale read is reached. It removes the last step of
this chain from the other end; this PR stops the failed connection from
being resurrected at all.
- #30950 guards the JS pool's `handleConnected` against the reverse
ordering within one read (a legitimately queued `onconnect` microtask
arriving after a synchronous `onclose`).
liooil pushed a commit that referenced this pull request Aug 7, 2026
### Repro

```sh
printf '{"name":"x","version":"1.0.0"}' > package.json
bun pm pkg set 'contributors[0]=alice'
```

On a release build (1.4.0 and current `main`) this exits 0 and writes
freed heap bytes into `package.json` as the property key:

```json
{
"name": "x",
"version": "1.0.0",
"P\x01\x00\x00\x00tors": {
  "\x00": "alice"
}
}
```

Depending on what was in the freed allocation the result is often not
valid JSON at all. Any `bun pm pkg set` key path containing `[index]`
hits it.

Under ASAN it is a deterministic `heap-use-after-free`:

```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 1
    #0 bun_js_printer::write_pre_quoted_string_inner   src/js_printer/lib.rs:1014
    #7 PmPkgCommand::save_package_json                 src/runtime/cli/pm_pkg_command.rs:909
freed by thread T0 here:
    #7  <Box<[u8]> as Drop>::drop
    #12 PmPkgCommand::set_value                        src/runtime/cli/pm_pkg_command.rs:661
previously allocated by thread T0 here:
    #10 <Box<[u8]> as From<&[u8]>>::from
    #11 PmPkgCommand::parse_key_path                   src/runtime/cli/pm_pkg_command.rs:583
```

<details>
<summary>full ASAN report</summary>

```
=================================================================
==16563==ERROR: AddressSanitizer: heap-use-after-free on address 0x73423c7c0670 at pc 0x00000f583cc5 bp 0x7fff2667e950 sp 0x7fff2667e948
READ of size 1 at 0x73423c7c0670 thread T0
    #0 0x00000f583cc4 in _RINvCs59Hqei94dXF_14bun_js_printer29write_pre_quoted_string_innerINtB2_16StdWriterAdapterQINtB2_6WriterNtB2_12BufferWriterEEKVNtNtB2_8Encoding4Utf8UECsgBGN0jRPILJ_11bun_bundler /workspace/bun/src/js_printer/lib.rs:1014:79
    #1 0x00000ebda439 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_characters_utf8 /workspace/bun/src/js_printer/lib.rs:2641:21
    #2 0x00000ebdb7a7 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_characters_e_string /workspace/bun/src/js_printer/lib.rs:4546:22
    #3 0x00000ebdb238 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_literal_e_string /workspace/bun/src/js_printer/lib.rs:3018:18
    #4 0x00000ebd2be2 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_property /workspace/bun/src/js_printer/lib.rs:4807:34
    #5 0x00000ebc0166 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_expr /workspace/bun/src/js_printer/lib.rs:3962:38
    #6 0x00000ee574c1 in bun_js_printer::print_json::<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>> /workspace/bun/src/js_printer/lib.rs:8071:13
    #7 0x00000c05c270 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::save_package_json /workspace/bun/src/runtime/cli/pm_pkg_command.rs:909:25
    #8 0x00000c060cfb in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:330:13
    #9 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32
    #10 0x00000bf919d0 in <bun_runtime::cli::package_manager_command::PackageManagerCommand>::exec /workspace/bun/src/runtime/cli/package_manager_command.rs:704:13
    #11 0x00000c3fbb87 in bun_runtime::cli::command::exec_pm /workspace/bun/src/runtime/cli/mod.rs:1591:34
    #12 0x00000c3f2b86 in bun_runtime::cli::command::start /workspace/bun/src/runtime/cli/mod.rs:1309:43
    #13 0x00000bfad16c in bun_runtime::cli::cli::start /workspace/bun/src/runtime/cli/mod.rs:573:27
    #14 0x00000bb3c034 in main /workspace/bun/src/bun_bin/lib.rs:230:5
    #15 0x77223ccc7ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #16 0x77223ccc7d64 in __libc_start_main csu/../csu/libc-start.c:360:3
    #17 0x0000099d1d1d in __wrap___libc_start_main /workspace/bun/build/debug/../../src/jsc/bindings/workaround-missing-symbols.cpp:487:12

0x73423c7c0670 is located 0 bytes inside of 12-byte region [0x73423c7c0670,0x73423c7c067c)
freed by thread T0 here:
    #0 0x000007ae192a in free crtstuff.c
    #1 0x00000bb3c5a7 in <std::alloc::System as core::alloc::global::GlobalAlloc>::dealloc /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/alloc/unix.rs:48:18
    #2 0x00000bb3be9a in __rustc::__rust_dealloc /workspace/bun/src/bun_bin/lib.rs:56:15
    #3 0x00001258b05f in alloc::alloc::dealloc_nonnull /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:128:14
    #4 0x0000125872fe in <alloc::alloc::Global>::deallocate_impl_runtime /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:229:22
    #5 0x000012586364 in <alloc::alloc::Global>::deallocate_impl /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:344:9
    #6 0x00001258d79c in <alloc::alloc::Global as core::alloc::Allocator>::deallocate /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:462:23
    #7 0x000012582946 in <alloc::boxed::Box<[u8]> as core::ops::drop::Drop>::drop /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1956:24
    #8 0x000012572e44 in core::ptr::drop_in_place::<alloc::boxed::Box<[u8]>> /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1
    #9 0x000011f8d429 in core::ptr::drop_in_place::<[alloc::boxed::Box<[u8]>]> /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1
    #10 0x00000ef6b73a in <alloc::vec::Vec<alloc::boxed::Box<[u8]>> as core::ops::drop::Drop>::drop /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:4258:13
    #11 0x00000ef69e64 in core::ptr::drop_in_place::<alloc::vec::Vec<alloc::boxed::Box<[u8]>>> /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1
    #12 0x00000c061846 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::set_value /workspace/bun/src/runtime/cli/pm_pkg_command.rs:661:5
    #13 0x00000c061038 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:325:13
    #14 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32
    #15 0x00000bf919d0 in <bun_runtime::cli::package_manager_command::PackageManagerCommand>::exec /workspace/bun/src/runtime/cli/package_manager_command.rs:704:13
    #16 0x00000c3fbb87 in bun_runtime::cli::command::exec_pm /workspace/bun/src/runtime/cli/mod.rs:1591:34
    #17 0x00000c3f2b86 in bun_runtime::cli::command::start /workspace/bun/src/runtime/cli/mod.rs:1309:43
    #18 0x00000bfad16c in bun_runtime::cli::cli::start /workspace/bun/src/runtime/cli/mod.rs:573:27
    #19 0x00000bb3c034 in main /workspace/bun/src/bun_bin/lib.rs:230:5
    #20 0x77223ccc7ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

previously allocated by thread T0 here:
    #0 0x000007ae1bc8 in malloc crtstuff.c
    #1 0x00000bb3c520 in <std::alloc::System as core::alloc::global::GlobalAlloc>::alloc /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/alloc/unix.rs:14:22
    #2 0x00000bb3be30 in __rustc::__rust_alloc /workspace/bun/src/bun_bin/lib.rs:56:15
    #3 0x00001258b335 in alloc::alloc::alloc /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:101:9
    #4 0x000012586b81 in <alloc::alloc::Global>::alloc_impl_runtime /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:210:73
    #5 0x0000125862b6 in <alloc::alloc::Global>::alloc_impl /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:332:9
    #6 0x00001258d86a in <alloc::alloc::Global as core::alloc::Allocator>::allocate /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:449:14
    #7 0x00001257dbd3 in <alloc::boxed::Box<[u8]>>::try_clone_from_ref_in /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:881:29
    #8 0x00001257da49 in <alloc::boxed::Box<[u8]>>::clone_from_ref_in /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:840:15
    #9 0x00001257d3f4 in <alloc::boxed::Box<[u8]>>::clone_from_ref /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:793:9
    #10 0x000012581e34 in <alloc::boxed::Box<[u8]> as core::convert::From<&[u8]>>::from /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed/convert.rs:77:9
    #11 0x00000c05a47f in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::parse_key_path /workspace/bun/src/runtime/cli/pm_pkg_command.rs:583:37
    #12 0x00000c061608 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::set_value /workspace/bun/src/runtime/cli/pm_pkg_command.rs:643:30
    #13 0x00000c061038 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:325:13
    #14 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32
```

</details>

### Cause

`parse_key_path` returned a `Vec<Box<[u8]>>`, and `set_value` /
`set_nested` inserted those boxed segments into the manifest AST by
reference: `E::Object::put` constructs `EString::init(key)`, whose
documented contract is that `key` is arena-owned (it records the slice,
it does not copy it). The vector is a local of `set_value`, so it
dropped before `exec_set` reached `save_package_json`, and the JSON
printer then read the dangling keys.

The non-bracket path in `set_value` did not have the bug: it borrowed
its segments straight out of the argv key, which outlives the whole
command. The bracket path differed only by the unnecessary boxing.

### Fix

`parse_key_path` now returns `Vec<&[u8]>`. Every segment is a literal
sub-slice of the input key, so nothing ever needed owning. With the
boxing gone, `set_value`'s separate non-bracket branch and its
`set_nested_simple` helper (which existed only to avoid the allocation)
were exact duplicates of the bracket path, so they are deleted and all
keys route through `parse_key_path` + `set_nested`.

`set_nested_simple`'s trailing `root.put(current_key, nested)` was a
no-op: `ExprData::EObject` is a `StoreRef` handle, so mutating the copy
returned by `root.get()` already mutates the stored object, and the put
re-stores the same handle. Dropping it with the function changes nothing
(and the prior bracket path, `set_nested`, never had it).

Intentionally not changed here: `set 'contributors[0]=alice'` produces
`"contributors": {"0": "alice"}`, an object keyed by the digit string,
rather than the array npm's `pkg set` creates, and `set 'array[]=x'`
still errors with `InvalidPath` instead of appending. Both are the npm
compat gap tracked in #22035, which is separate from the memory safety
of the key names and is not closed by this PR.

### Verification

New test in `test/cli/install/bun-pm-pkg.test.ts` reparses the written
file and asserts the exact object. Without the fix it fails on release
(`SyntaxError: JSON Parse error: Invalid escape character x`) and on the
ASAN debug build (the child aborts on the use-after-free). With the fix
the full `bun-pm-pkg.test.ts` suite passes (74 pass, 0 fail).
liooil pushed a commit that referenced this pull request Aug 7, 2026
…buffer cannot be allocated (#33326)

Fixes a `Segmentation fault at address 0x00000040` (sometimes
`0x00000030`) reported from Windows x64 builds, crashing inside
boringssl's record copy from uSockets' TLS read loop:

```
memcpy                            src/vctools/crt/vcruntime/src/string/amd64/memcpy.asm
bssl::OPENSSL_memcpy              vendor/boringssl/crypto/internal.h:868
SSL_peek                          vendor/boringssl/ssl/ssl_lib.cc:947
SSL_read                          vendor/boringssl/ssl/ssl_lib.cc:918
us_internal_ssl_on_data           packages/bun-usockets/src/crypto/openssl.c:1797
us_internal_dispatch_ready_poll   packages/bun-usockets/src/loop.c:600
uv__fast_poll_process_poll_req    vendor/libuv/src/win/poll.c:208
uv_run                            vendor/libuv/src/win/core.c:737
```

## Cause

`us_internal_init_loop_ssl_data` (`openssl.c:677`) allocates one 512 KiB
plaintext buffer per event loop, lazily, on the loop's first TLS socket,
and never checked the result:

```c
loop_ssl_data->ssl_read_output =
    us_malloc(LIBUS_RECV_BUFFER_LENGTH + LIBUS_RECV_BUFFER_PADDING * 2);
```

With `ssl_read_output == NULL`, every later `SSL_read` hands boringssl

```c
loop_ssl_data->ssl_read_output + LIBUS_RECV_BUFFER_PADDING + read
```

as its plaintext destination, so the first record of application data
memcpy's to `NULL + 32`. `SSL_peek`'s `OPENSSL_memcpy(buf, ...)` at
`ssl_lib.cc:947` is the write, and the access violation confirms it is a
write fault.

`0x30`/`0x40` rather than `0x20` is memcpy's destination-alignment
preamble (`dst += VEC_SIZE; dst &= ~(VEC_SIZE - 1)`), which moves the
first faulting store for copies larger than eight vector registers.
Measured on the copy sizes a real TLS record produces:

| memcpy variant | first faulting store for `dst = NULL + 32` |
| --- | --- |
| 32-byte vectors (AVX) | `0x40` |
| 16-byte vectors (SSE) | `0x30` |

So the two strikingly stable fault addresses are just CPU dispatch
across the affected machines, and `read` is always `0`: the crash is
always the connection's first record of application data.

Only Windows reports it because Linux and macOS overcommit, so a 512 KiB
`malloc` there effectively never returns NULL. Windows fails the commit
cleanly, and the loop's much smaller `us_calloc` still succeeds out of
an already-committed page, leaving exactly the observed shape: a valid
`loop_ssl_data` whose `ssl_read_output` is NULL.

## Fix

- Null-check the buffer allocation, the `us_calloc` of `loop_ssl_data`,
and the `BIO_meth_new`/`BIO_new` calls beside them, and route the
failure through Bun's out-of-memory crash path (`Bun__outOfMemory`, new
C entry point next to `Bun__panic`). The process now dies with `Bun ran
out of memory` and a stack trace that names the allocation, instead of
faulting on the first TLS byte.
- Apply the same check to the sibling site: `recv_buf`/`send_buf` in
`us_internal_loop_data_init` are the same unchecked
`malloc(LIBUS_RECV_BUFFER_LENGTH + LIBUS_RECV_BUFFER_PADDING * 2)`. A
NULL `recv_buf` does not fault, it makes every read on the loop fail
with `EFAULT` for the life of the process, which is worse to diagnose.
- `us_internal_free_loop_ssl_data` left `loop->data.ssl_data` dangling,
which defeats the `if (!loop->data.ssl_data)` guard the init function
relies on. It now clears the field.

A 512 KiB `malloc` effectively never returns NULL on an overcommitting
kernel, so the failure path needs the existing socket fault injector to
be reachable from a test. This adds an `ssl_loop_buffer` rule to it,
which like the rest of the injector is compiled out of release builds.

## Verification

The new test spawns a child that arms `ssl_loop_buffer` before its first
TLS socket and asserts it reports out of memory rather than reaching a
read loop.

Reverting only `if (!loop_ssl_data->ssl_read_output)
Bun__outOfMemory();` reproduces the reported crash exactly, on Linux,
from that same fixture: same fault address, same frames, same boringssl
source lines.

<details>
<summary>Reproduction on the unfixed build (<code>bun bd</code>,
ASAN)</summary>

```
==19592==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000040
==19592==The signal is caused by a WRITE memory access.
==19592==Hint: address points to the zero page.
    #0 __memcpy_evex_unaligned_erms
    #1 bssl::OPENSSL_memcpy(void*, void const*, unsigned long) vendor/boringssl/crypto/internal.h:868:10
    #2 SSL_peek vendor/boringssl/ssl/ssl_lib.cc:947:3
    #3 SSL_read vendor/boringssl/ssl/ssl_lib.cc:918:13
    #4 us_internal_ssl_on_data packages/bun-usockets/src/crypto/openssl.c:1847:21
    #5 us_internal_dispatch_ready_poll packages/bun-usockets/src/loop.c:625:38
```

With the fix:

```
panic(main thread): Bun ran out of memory
Bun__outOfMemory              src/bun_bin/phase_c_exports.rs:81:5
us_internal_init_loop_ssl_data packages/bun-usockets/src/crypto/openssl.c:696:42
us_internal_ssl_attach        packages/bun-usockets/src/crypto/openssl.c:1275:3
```

</details>

`test/js/node/tls/tls-syscall-fault.test.ts` (11 pass),
`test/js/bun/util/socket-fault-injection.test.ts` (15 pass), plus
`socket-syscall-fault`, `serve-syscall-fault` and `fetch-syscall-fault`
(19 pass) are green. The three failures in `test/js/node/tls/` on this
machine are pre-existing: two also fail on an unmodified 1.4.0, and
`tls.connect should ignore invalid NODE_EXTRA_CA_CERTS` takes 5.75s,
just over the 5s local default (CI triples the per-test timeout for ASAN
builds).

## Teardown audit

The report also asked whether a socket can reach
`us_internal_ssl_on_data` after its loop's SSL data has been freed.
`us_internal_free_loop_ssl_data` is only reachable from `us_loop_free`,
and the only loop Bun frees today is `SpawnSyncEventLoop`'s, which never
has a TLS socket attached (its `loop->data.ssl_data` is always NULL, so
the free is a no-op). So that is not the cause here.

It is worth noting separately that the libuv `us_loop_free`
(`eventing/libuv.c:201-206`) calls `us_internal_loop_data_free(loop)`
and *then* runs `uv_run(loop->uv_loop, UV_RUN_NOWAIT)`, which is a full
libuv iteration that can dispatch socket poll callbacks into the
just-freed `recv_buf` and `ssl_data`. The POSIX `us_loop_free`
(`eventing/epoll_kqueue.c:56-60`) has no such window. It is unreachable
today for the reason above, so it is left out of this PR rather than
changing loop teardown ordering without a test that can exercise it.
liooil pushed a commit that referenced this pull request Aug 7, 2026
…#34693)

## Use-after-free in `H2FrameParser::on_native_writable`

Fleet ASAN fuzz hit (p-h2c cleartext harness, seed 1,
`server-conn.recv.*:A8`):

```
use-after-poison READ 8 (shadow f7 = user poison, HiveArray slot re-poison)
  #0 Vec::len (write_buffer)
  #2 has_backpressure          h2_frame_parser.rs:3276
  #3 on_native_writable        h2_frame_parser.rs:9605
  #4 NewSocket<true>::on_writable  socket_body.rs:894
  #8 us_internal_ssl_on_writable   bun-usockets openssl.c:1851
allocated by: HiveArray Fallback<H2FrameParser,256>, H2FrameParser::constructor
```

### Cause

`on_native_writable` loops `flush()` and checks `has_backpressure()`
between iterations. `flush()` re-enters JS via `flush_stream_queue` ->
`dispatch_write_callback` / `onStreamEnd` / `onWantTrailers`. A callback
that destroys the session reaches `detach_native_callback`, dropping the
socket's `+1` on the parser. If that was the last external ref,
`flush()`'s own keepalive is all that remains and drops on return, so
the next `has_backpressure()` reads a HiveArray slot that was just
`drop_in_place`'d and re-poisoned by `POOL.put`.

`on_native_read` already takes a `keepalive()` for exactly this reason
(h2_frame_parser.rs:9590); `on_native_writable` did not.

In release builds there is no poison: the same ordering is a silent
use-after-free in every `node:http2` server/client on a native socket.
The read of a stale `write_buffer.len()` can satisfy the loop condition
and send the next `flush()` into UAF writes on the freed parser.

### Fix

- Take a `keepalive()` for the extent of `on_native_writable`, mirroring
`on_native_read`.
- `NativeCallbacks::on_data`/`on_writable`: copy the raw `*mut
H2FrameParser` out of the enum before dispatching, so the
`JsCell<NativeCallbacks>` borrow does not span a re-entrant
`detach_native_callback` that overwrites the cell.

### Test

`test/js/node/http2/node-http2-writable-destroy-fixture.ts` reproduces
the exact fleet stack under ASAN by faulting `send`/`writev` to 0
(backpressure, arms WRITABLE), queuing a DATA frame whose write callback
runs `session.destroy()` + `Bun.gc(true)`, then clearing the fault so
the writable event drains the queue inside `on_native_writable`. Added
to `node-http2-syscall-fault.test.ts` as an ASAN-gated subprocess test.

<details><summary>Fail-before ASAN report (matches the fleet
hit)</summary>

```
==ERROR: AddressSanitizer: use-after-poison on address 0x... 
READ of size 8 at 0x... thread T0
    #2 H2FrameParser::has_backpressure h2_frame_parser.rs:3276:33
    #3 H2FrameParser::on_native_writable h2_frame_parser.rs:9605:21
    #4 NativeCallbacks::on_writable socket_body.rs:3960:20
    #5 NewSocket<false>::on_writable socket_body.rs:894:39
allocated by thread T0 here:
    ... Fallback<H2FrameParser, 256>::new_boxed hive_array.rs:668
    ... H2FrameParser::constructor h2_frame_parser.rs:9800
SUMMARY: AddressSanitizer: use-after-poison ... Vec<u8>::len
```
</details>

<!-- 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/node/http2/node-http2-syscall-fault.test.ts

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…e (#35255)

`test/js/bun/http/serve-protocols.test.ts` has been going red on main
(build 78445 darwin-x64 hard, 78462 debian-11-aarch64 hard, plus
78300/78419 with retries), always as

```
error: HTTP3StreamReset fetching "https://127.0.0.1:<port>/echo"
✗ Bun.serve over http/3 > POST echo 1000000 bytes [20169.37ms]
```

Reproduced on Linux by looping the file: the h3 subset alone fails about
17 of 100 runs.

## Cause

The ten concurrent h3 tests share one lsquic client engine and one
unconnected UDP socket. `bsd_create_udp_socket()` sets `IP_RECVERR` on
every UDP socket (for `node:dgram`'s error surfacing, #28827), including
QUIC's. When a finished test `proc.kill()`s its server, the client
session is still in the engine and keeps scheduling retransmits /
`NEW_CONNECTION_ID` to an unbound port. With `IP_RECVERR` on, the
resulting ICMP port-unreachable is queued on the shared socket and the
next `sendmmsg` returns `-1 ECONNREFUSED`, even though that call is
sending a datagram to a live peer.

`us_quic_packets_out` reports that as a short return, lsquic clears
`ENPUB_CAN_SEND` for the whole engine and only its one-second
`resume_sending_at` failsafe re-enables it. With several dead sessions
generating ICMPs, every failsafe retry fails the same way and the live
1MB upload never advances; the 20s in CI is two idle-timeout rounds
through `retry_or_fail`.

While tracing that I also found an unsigned underflow in lsquic's
`send_batch` requeue loop: when the first unsent spec in a batch
coalesces multiple packets (`pack_off[0] == 0`, `iovlen > 1`), `end =
&batch->packets[off - 1]` indexes with `UINT_MAX` and only the last
packet of the coalesced group is returned to the connection. The earlier
ones are the INIT ACK and the HSK CRYPTO carrying the client Finished,
so the peer can never complete the handshake. This is the same hang
reached from a different direction (real EAGAIN backpressure instead of
stale ICMP).

## Fix

- `IP_RECVERR` is now opt-in via `LIBUS_UDP_LINUX_RECVERR`, set by
`us_create_udp_socket` when a `recv_error_cb` is provided. `node:dgram`
always passes one and keeps the option; QUIC passes `NULL` and no longer
gets it. This matches libuv's `UV_UDP_LINUX_RECVERR` gating that `bsd.c`
already cited.
- `us_quic_packets_out()` retries once on a non-`EAGAIN`/`ENOBUFS` send
failure before reporting a short return, so a stale `sk_err` that does
surface cannot pause the engine. Both the `sendmmsg` and per-packet
paths now go through `US_FAULT_CHECK(US_FAULT_SENDMSG, ...)` so the
short-return path is reachable from tests.
- `patches/lsquic/requeue-unsent-coalesced.patch` rewrites the requeue
loop's bounds as `[off, off+count)` so every packet in an unsent
coalesced datagram is returned to the connection. The same underflow is
present in upstream lsquic master; I will open a PR there separately.
- `serve-protocols.test.ts` now stops each fixture server gracefully on
stdin close (`server.stop(true)`), matching `serve-http3.test.ts`, so
the pooled client session sees `CONNECTION_CLOSE` instead of leaving the
engine retransmitting to unbound ports.
- `test/js/web/fetch/fetch-http3-syscall-fault.test.ts` injects `EAGAIN`
on the coalesced handshake datagram (the `pack_off[0]==0`, `iovlen>1`
spec the lsquic patch fixes), a one-shot `ECONNREFUSED` that the
retry-once branch consumes, and a burst of `EAGAIN` that the `on_drain`
path recovers from.

## Verification

Release build, looped:

| | before | after |
| --- | --- | --- |
| `serve-protocols -t "http/3"` | 17/100 fail | 2/100 fail |
| `serve-protocols` (full) | 7/100 fail | 4/200 fail |

Debug+ASAN: `serve-protocols`, `serve-http3` (46), `fetch-http3-client`
(52), `fetch-http3-adversarial` (29), `fetch-http3-syscall-fault` (3)
and `dgram.test.ts` all pass, 211 tests total.

The residual ~1-2% is a separate pre-existing bug (the client's 36-byte
HSK CRYPTO is buffered but never flushed when `drain_send_body` writes
the whole 1MB body synchronously from `on_stream_open`); I've handed
that off as its own issue. With CI's retry it is well under the flake
threshold.

### Gate note

The fault-injection hook that makes the new test deterministic lives in
`packages/bun-usockets/src/quic.c`, so `git stash -- src/ packages/`
removes it along with the fix and the fault never fires. The lsquic
piece lives in `patches/` and `scripts/`, which the stash does not
touch. That means a single stashed run passes (no fault, no stall) and a
single unstashed run passes (fault fires, fix handles it), and the gate
cannot distinguish them mechanically. The 300-iteration probe above is
the evidence; the fault-injection tests pin the behavior going forward.

<details>
<summary>lsquic debug trace of the stall</summary>

```
engine: packets out returned 0 (out of 1)
[C919…] event: unsent packet #15 ACK_FREQUENCY, size 36
[C919…] sendctl: packet #15 has been delayed
engine: send_packets_out: sent 0 packets
…                                                    <- no "can send again"; nothing for 1s
engine: failsafe activated: resume sending packets again after timeout
engine: packets out returned 0 (out of 10)           <- fails again, live conn's #207 included
```

and for the underflow, a batch with `pack_off[0]=0`, `iovlen[0]=3`:

```
engine: packets out returned 0 (out of 2)
event: unsent packet #3 ACK PADDING, size 1059
event: unsent packet #4 ACK CRYPTO, size 87
event: unsent packet #5 NEW_CONNECTION_ID, size 54
event: unsent packet #6 STREAM, size 114
sendctl: packet #6 has been delayed
sendctl: packet #5 has been delayed
…                                                    <- #3 and #4 never requeued
[WARN] sendctl: send history gap 2 - 5
```
</details>

<!-- 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/web/fetch/fetch-http3-syscall-fault.test.ts

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
liooil pushed a commit that referenced this pull request Aug 7, 2026
…rier (#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 -->
liooil pushed a commit that referenced this pull request Aug 7, 2026
…e cache (#37034)

### Problem

On the `13 x64-asan` lane, a test that exercises non-ISO Temporal
calendars from a test callback can abort after a fully green run with a
LeakSanitizer report. Seen in build 89504 on #37024, whose
`test/js/bun/bun-object/deep-equals-temporal.test.ts` uses
`[u-ca=hebrew]`:

```
Direct leak of 624 byte(s) in 1 object(s) allocated from:
    #1 icu_75::HebrewCalendar::clone() const
    #2 icu_75::Calendar::createInstance(icu_75::TimeZone*, icu_75::Locale const&, UErrorCode&)
    #3 ucal_open_75
    #4 JSC::TemporalCore::buildCalendarTemplate(WTF::AbstractLocker const&, unsigned int)
    #5 JSC::TemporalCore::withCalendar<JSC::TemporalCore::calendarYear(...)::$_0>(...)
```

The CI annotation titles this `direct leak of 624b in {closure#0}
(src/jsc/JSValue.rs:1664:22)` because that is the first in-repo frame
(the test-runner's `JSValue::call`); everything below it is WebKit/ICU.

### Cause

`TemporalCore::withCalendar`
(`vendor/WebKit/.../temporal/core/CalendarICUBridge.cpp`) keeps up to 8
open `UCalendar` templates in a process-lifetime `LazyNeverDestroyed`
`TinyLRUCache`, one per calendar ID (non-ISO arithmetic, plus pure-ISO
`PlainDateTime.prototype.with`, which reaches the same path unguarded);
LRU eviction `ucal_close`s them, so the set is bounded. The
`CalendarCacheEntry` that owns each `UCalendar` is
`WTF_MAKE_TZONE_ALLOCATED` (bmalloc), which LSan does not scan, so the
libc-allocated `UCalendar` (and the ICU `TimeZone` inside it) is
reported as a direct leak even though it is reachable. Whether a given
run aborts depends on whether some stale stack or register value still
points at the ICU object when LSan scans at exit, hence the
intermittence.

This is the calendar twin of the already-suppressed
`TemporalCore::withTimeZone` entry (same cache design, same
TZone-allocated owner).

### Fix

- Add a `leak:TemporalCore::buildCalendarTemplate` suppression to
`test/leaksan.supp`, mirroring the `withTimeZone` entry. The pattern
anchors on the template builder rather than `withCalendar` itself so
that a future real leak inside one of the many op lambdas `withCalendar`
runs would still be reported; every cached-template allocation carries
the builder frame. (`withTimeZone` has no such builder frame, its
`ucal_open` is inline, so that entry keeps its existing pattern.)
- Drop the `test/no-validate-leaksan.txt` escape hatch #37024 added for
`deep-equals-temporal.test.ts`, re-enabling leak validation for it; that
file exercises the suppressed path on the asan lane.

### Verification

On a debug ASAN build, running `bun test
test/js/bun/bun-object/deep-equals-temporal.test.ts` under the CI
leak-validation env (`BUN_DESTRUCT_VM_ON_EXIT=1`,
`detect_leaks=1:abort_on_error=1`, repo suppression file):

- with the new entry: clean exit, 5/5 runs
- without it: LSan abort with the calendar-template stacks above, 3/3
runs

A standalone probe exercising 8 non-ISO calendars plus pure-ISO
`PlainDateTime.with` from a timer callback shows the same split (10/10
aborts without, 10/10 clean with; `print_suppressions=1` attributes
exactly the ICU template allocations to the new entry). Top-level module
code cannot reproduce this: its allocation stacks carry
`JSC::JSModuleLoader::evaluateNonVirtual`, which the suppression file
already covers wholesale. An ASAN-gated test pinning the entry was part
of an earlier revision and was dropped per review; the re-enabled
`deep-equals-temporal.test.ts` covers the path in CI instead.

The Expect-wrapper shutdown leak mentioned in the dropped no-validate
comment is a separate issue tracked in #32180: that is `bun test`'s own
finalizer-owned memory, while this cache deliberately survives VM
teardown, so #32180 would not prevent this report.

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 1 · docs-only change; test-proof not
applicable

<!-- robobun:evidence:end -->

---------

Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
liooil added a commit that referenced this pull request Aug 7, 2026
Add a 'Derived from Bun' section to the top of README.md (and the Chinese
mirror): Poly is a fork of Bun (originally oven-sh/bun); the Bun source tree
and history are part of this repository, with a Rust host and embedded
RustPython layered on top. Record the fork origin commit and the upstream
tracking policy, and link the build section to the new section instead of
duplicating the origin commit hash.

Co-authored-by: xiteng <liuxiteng@baosight.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant