Skip to content

Navigation Menu

Sign in
Sign up

deps: update libarchive to v3.8.9 - #2

Open
github-actions[bot] wants to merge 1 commit into
main from
deps/update-libarchive
Open

deps: update libarchive to v3.8.9 #2
github-actions[bot] wants to merge 1 commit into
main from
deps/update-libarchive

Conversation

@github-actions

@github-actions github-actions Bot commented Feb 22, 2026
edited
Loading

Copy link
Copy Markdown

What does this PR do?

Updates libarchive to version v3.8.9

Compare: libarchive/libarchive@9525f90...27cbc78

Auto-updated by this workflow

@github-actions github-actions Bot changed the title (削除) deps: update libarchive to v3.8.5 (削除ここまで) (追記) deps: update libarchive to v3.8.6 (追記ここまで) Mar 15, 2026
@github-actions github-actions Bot changed the title (削除) deps: update libarchive to v3.8.6 (削除ここまで) (追記) deps: update libarchive to v3.8.7 (追記ここまで) Apr 19, 2026
igorls pushed a commit that referenced this pull request Apr 25, 2026
...-sh#29330)
## What
Adds an early-return at the top of `ResumableSink.cancel()` when `status
== .done`, so `onEnd` fires at most once.
Fixes oven-sh#20740
Fixes oven-sh#21463
## Why
When a `fetch()` with a `ReadableStream` request body is aborted,
`ResumableSink.cancel()` is called from `FetchTasklet.abortListener()`
(FetchTasklet.zig:1203). The HTTP thread then completes with failure and
`onProgressUpdate`'s reject path calls `sink.cancel()` a second time at
FetchTasklet.zig:576 (and `onBodyReceived` at :342) — `this.sink` is
only nulled in `clearSink←clearData←deinit`.
`cancel()` (ResumableSink.zig:228) guarded against re-entry only for
`status == .piped`. For the JS-route sink it relied on
`#js_this.tryGet()` returning null after `detachJS()`, but
`JSRef.downgrade()` (JSRef.zig:153-160) preserves the wrapper value as
`.weak = <wrapper>`, and `tryGet()` (JSRef.zig:111) returns non-null for
any non-empty weak. So the second `cancel()` re-enters the block and
re-invokes `onEnd` → `FetchTasklet.writeEndRequest` → unconditional
`defer this.deref()` (FetchTasklet.zig:1286).
That second deref releases the single ref taken in
`startRequestStream()` (FetchTasklet.zig:300) twice. Ref-count math:
init(1) + queue(1) + startRequestStream(1) = 3 → cancel#1 deref → 2 →
derefFromThread → 1 → cancel#2 deref → 0 → `deinit()`/`destroy()` runs
*inside* `onProgressUpdate`, then its defer at :471-477 does
`this.mutex.unlock()` + `this.deref()` on freed memory.
`jsEnd()` already has an `isDetached()` guard for the same reason;
`cancel()` was missing the equivalent. Using `status == .done` (rather
than `isDetached()`) keeps the `.piped` branch reachable since piped
sinks never set `#js_this` to `.strong`.
## Test
`test/js/web/fetch/fetch-abort-stream-body.test.ts` reproduces the
use-after-free in a debug/ASAN build:
```
[fetchtasklet] abortListener
[fetchtasklet] writeEndRequest hasError? true <- cancel #1
[fetchtasklet] callback success=false ...
[fetchtasklet] onProgressUpdate
[fetchtasklet] onReject
[fetchtasklet] writeEndRequest hasError? true <- cancel #2 (over-deref)
[fetchtasklet] deinit
==40720==ERROR: AddressSanitizer: use-after-poison ... in onProgressUpdate
```
igorls pushed a commit that referenced this pull request Apr 25, 2026
...yToRoot (oven-sh#29483)
Fuzzilli found a use-after-poison in the runtime auto-install path.
`enqueueDependencyToRoot` passed
`&lockfile.buffers.dependencies.items[dep_id]` into
`enqueueDependencyWithMainAndSuccessFn`. When the manifest for the
requested package is already cached (on disk or in memory) but the
extracted tarball is not, control reaches
`getOrPutResolvedPackageWithFindResult`, which calls
`Lockfile.Package.fromNPM`. That grows `buffers.dependencies` via
`ensureUnusedCapacity` to make room for the package's own dependencies,
reallocating the backing storage. The subsequent `.extract` branch then
read `dependency.behavior.isRequired()` from the freed buffer.
```
#0 getOrPutResolvedPackageWithFindResult PackageManagerEnqueue.zig:1520 dependency.behavior.isRequired()
#1 getOrPutResolvedPackage PackageManagerEnqueue.zig:1778
#2 enqueueDependencyWithMainAndSuccessFn PackageManagerEnqueue.zig:523
#3 enqueueDependencyToRoot PackageManagerEnqueue.zig:321
#4 Resolver.enqueueDependencyToResolve resolver.zig:2356
...
oven-sh#14 Bun__resolveSync
oven-sh#15 functionImportMeta__resolveSyncPrivate (runtime require() path)
```
Two changes:
- `enqueueDependencyToRoot` now copies the `Dependency` to the stack
before taking its address, matching every other caller of
`enqueueDependencyWithMainAndSuccessFn` (`processDependencyListItem`,
`processPeerDependencyList`, etc.).
- The one read that ran after `fromNPM` now uses the `behavior`
parameter that was already passed by value, instead of re-dereferencing
`dependency`.
Repro (debug/ASAN only): auto-install a package with a warm on-disk
manifest but no extracted tarball — `fromNPM` appending even a single
dependency forces a realloc of the one-entry buffer. The new test warms
the cache, removes the extracted tarballs, and runs `require()` via `-e`
so it goes through `Bun__resolveSync` → `enqueueDependencyToRoot`.
igorls pushed a commit that referenced this pull request Apr 25, 2026
`ResolveMessage.create` stored the `referrer` path via `Fs.Path.init`
without cloning. Every caller passes a temporary buffer — the `toUTF8()`
of a `bun.String` that is `deinit()`'d on return — so reading
`.referrer` after the creating frame unwound was a use-after-free.
Found by Fuzzilli as a flaky `use-after-poison` via `vi.mock()` →
`Bun__resolveSyncWithSource` → `resolveMaybeNeedsTrailingSlash`, but it
reproduces deterministically under ASAN with any non-ASCII source path:
```js
let err;
try {
 Bun.resolveSync("./does-not-exist", "/tmp/café-🎉/file.js");
} catch (e) { err = e; }
Bun.gc(true);
err.referrer; // use-after-poison
```
```
==3080==ERROR: AddressSanitizer: use-after-poison on address 0x77cca9db0000 ...
READ of size 44 at 0x77cca9db0000 thread T0
 #0 in __asan_memcpy
 #1 in Zig::toStringCopy(ZigString) helpers.h:217
 #2 in ZigString__toValueGC bindings.cpp:3402
 #3 in ZigString.toJS ZigString.zig:57
 #4 in ResolveMessage.getReferrer ResolveMessage.zig:221
```
In release builds the first 8 bytes of the returned referrer are
overwritten by mimalloc's free-list pointer instead of crashing.
Clone the referrer in `create()` and free it in `finalize()`. Also
`deinit()` the `toUTF8()` temporaries in `processFetchLog` now that
`create()` copies.
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
...en-sh#29910)
## 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 oven-sh#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>
igorls pushed a commit that referenced this pull request May 6, 2026
Closes oven-sh#29925
Closes oven-sh#22808
Closes oven-sh#24019
## What does this PR do?
Fixes a bug where `Bun.RedisClient` would permanently reject every
command with `Connection has failed` after the client entered the failed
state (via reconnect exhaustion, manual `close()`, or a fatal socket
error). Calling `client.connect()` did not recover the client — the
process had to be restarted.
## Root cause
Pre-refactor (before oven-sh#23141) `.failed` was a connection status and
`doConnect` handled it explicitly:
```zig
.failed => {
 this.client.flags.is_reconnecting = true;
 this.client.retry_attempts = 0;
 this.reconnect();
},
```
The refactor folded `.failed` into `.disconnected` and a new
`flags.failed` boolean but never wired up the reset path. Two things
stayed sticky:
1. **`flags.failed`** — `send()` short-circuits on this and immediately
rejects with `Connection has failed`. Once set in `failWithJSValue`,
nothing ever cleared it.
2. **`flags.is_authenticated`** — kept `true` from the prior successful
session, so when the new socket's HELLO response arrived,
`handleResponse` skipped `handleHelloResponse` (which is guarded by `if
(!this.flags.is_authenticated)`) and silently discarded the response.
The client never transitioned back to `.connected` and `connect()` would
hang until the connection timeout fired.
## Fix
Two small resets:
- `doConnect` (src/valkey/js_valkey.zig) clears `flags.failed` alongside
`is_manually_closed` so an explicit `connect()` hands the client a clean
slate.
- `onOpen` (src/valkey/valkey.zig) clears `flags.failed`,
`is_authenticated`, and `is_selecting_db_internal` so a fresh socket
properly replays the HELLO handshake — matching what `onClose`'s
auto-reconnect branch already does at L502–504.
## Verification
New regression test at `test/regression/issue/29925.test.ts` spawns a
local `redis-server` on a random port, drives the client into the failed
state via `close()` (same terminal state as max-retries exhaustion),
then asserts `connect()` recovers the client and subsequent commands
complete round-trip. Gate check confirms the test times out without the
fix.
Also manually verified:
- oven-sh#22808: tight `close()` + `connect()` + `send("FLUSHALL", ["SYNC"])`
loop that previously locked up on iter 1 now runs cleanly across many
iterations.
- oven-sh#24019: after max-retries exhaustion during a redis restart,
`client.connect()` recovers the client instead of returning `connected:
true` while the next command still rejects.
Reproduction from oven-sh#29925:
```
$ bun /tmp/repro.ts
first set: Max reconnection attempts reached
subsequent #0: Connection has failed ← forever
subsequent #1: Connection has failed
subsequent #2: Connection has failed
```
After the fix, `await client.connect()` brings the client back online
and the next `set`/`get` pair succeeds.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
...double-free in deinit (oven-sh#29988)
## Repro
Dev server with a directory watch that has two pending
resolution-failure dependencies (`./sub/a` at index 0, `./sub/b` at
index 1). Create `sub/a.ts` so dep 0 resolves; because it is not the
tail slot, `freeDependencyIndex(0)` pushes index 0 onto
`dependencies_free_list`. Shut the server down.
```
==ERROR: AddressSanitizer: negative-size-param: (size=-6148914691236517206)
 #1 mem.Allocator.free
 #2 bake.DevServer.deinit /workspace/bun/src/bake/DevServer.zig:686
 #3 bun.js.api.server.NewServer(.http,.debug).deinitIfWeCan
Address 0xaaaaaaaaaaaaaaaa is a wild pointer
```
## Cause
`DirectoryWatchStore.freeDependencyIndex` frees `dep.specifier` and (in
debug) sets the whole slot to `undefined`, then pushes the index onto
`dependencies_free_list`. The slot stays in `dependencies.items`.
`DevServer.deinit` iterates every `dependencies.items` slot and calls
`alloc.free(watcher.specifier)` without consulting the free list, so
free-list slots are freed a second time. In debug builds the `undefined`
(0xAA...) slice trips ASAN's negative-size check; in release it is a
straight double-free. `memoryCost` has the same blind iteration and
would read `.len` from freed memory.
## Fix
After freeing, write an empty slice back into `specifier` so the slot is
safe to revisit: `alloc.free(&.{})` is a no-op and `.len == 0`.
## Verification
New test `deinit with a free-list slot in
DirectoryWatchStore.dependencies` in `test/bake/dev/bundle.test.ts`
arranges the free-list slot and lets the harness's graceful-exit call
`deinit`.
- `git stash -- src/ && bun bd test ... -t 'deinit with a free-list slot'`
→ 3/3 **fail** (ASAN abort at DevServer.zig:686)
- with fix → 3/3 **pass**
- adjacent `removing 'use client' from a component with a pending
resolution failure` test still passes
---------
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
...ven-sh#29971)
## What
`FileSystemRouter`'s constructor (and `reload()`) initialize the error
log with the arena allocator:
```zig
const allocator = arena.allocator();
...
var log = Log.Log.init(allocator);
```
When route loading produces errors, the error paths did:
```zig
arena.deinit();
globalThis.allocator().destroy(arena);
return globalThis.throwValue(try log.toJS(...)); // reads arena-backed msgs.items
```
`log.msgs.items` is backed by the arena, so `log.toJS()` reads freed
memory. ASAN reports `use-after-poison` in `logger.Log.toJS`.
## Repro
```js
// pages/[foo.tsx — missing closing bracket
new Bun.FileSystemRouter({ style: "nextjs", dir: "./pages", fileExtensions: [".tsx"] });
```
Debug (ASAN) build:
```
AddressSanitizer: use-after-poison ...
 #1 in logger.Log.toJS (src/logger.zig:733)
 #2 in FileSystemRouter.constructor (src/bun.js/api/filesystem_router.zig:149)
```
## Fix
Build the JS error value first (while the arena is still live —
`BuildMessage.create` / `ResolveMessage.create` clone the msg into
`globalThis.allocator()`), then free the arena, then throw. Applied to
all four `log.toJS()` call sites across `constructor()` and `reload()`.
## Verification
- `git stash -- src/ && bun bd test filesystem_router.test.ts -t
'invalid route'` → **fail** (ASAN crash in subprocess)
- `git stash pop && bun bd test filesystem_router.test.ts -t 'invalid
route'` → **pass**, error message is `Route is missing a closing
bracket]`
- All 19 existing `filesystem_router.test.ts` tests pass.
---------
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
...es (oven-sh#30077)
## What
When a chunked (or HTTP/3) request body exceeds `maxRequestBodySize`,
`onBufferedBodyChunk` writes the 413 directly on the raw uWS response:
```zig
resp.writeStatus("413 Payload Too Large");
resp.endWithoutBody(comptime !http3);
```
`internalEnd` → `markDone()` nulls `onAborted`, so when the socket
closes no abort ever fires to detach `ctx.resp` or release the base ref.
`this.resp` is left pointing at a completed response whose socket is
about to be freed by `us_internal_free_closed_sockets`.
If the fetch handler returned a pending Promise:
- **resolve**: `handleResolve` → `isAbortedOrEnded()` is false
(`this.resp != null`) → `render()` → `runCorkedWithType` corks the freed
socket → **heap-use-after-free** (ASAN trace below).
- **reject**: `handleReject` reads `resp.hasResponded()` off freed
memory, sees `true`, skips the error handler, and returns without ever
releasing the base ref → **RequestContext leaks**
(`server.pendingRequests` never returns to 0).
## Fix
Route through `this.endWithoutBody()` (the `RequestContext` wrapper)
instead of the raw `resp.endWithoutBody()`. That path does
`detachResponse()` (nulls `this.resp`, clears
`onData`/`onAborted`/`onTimeout`) and `deref()` (releases the base ref),
matching every other end path in this file.
The body promise is rejected with the specific `"Request body exceeded
maxRequestBodySize"` error *before* `endWithoutBody()` so
`endRequestStreaming()` doesn't overwrite it with a generic
`ConnectionClosed`. `has_written_status` is set so any later
`renderMissing`/`renderMetadata` knows the status line is already
committed.
## Repro
```
==ERROR: AddressSanitizer: heap-use-after-free
 #0 us_socket_group socket.c:77
 #1 uWS::AsyncSocket<false>::getLoopData() AsyncSocket.h:69
 #2 uWS::AsyncSocket<false>::isCorked() AsyncSocket.h:141
 #3 uWS::HttpResponse<false>::cork(...) HttpResponse.h:647
 #4 uws_res_cork libuwsockets.cpp:1740
 #5 ...runCorkedWithType Response.zig:299
 #6 ...doRenderBlob RequestContext.zig:1942
 ...
 oven-sh#11 ...handleResolve RequestContext.zig:220
 oven-sh#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>
igorls pushed a commit that referenced this pull request May 6, 2026
...to prevent UAF (oven-sh#30057)
## What
`UDPSocket.sendMany()` and `UDPSocket.send()` both captured raw pointers
into the payload's ArrayBuffer backing store (or borrowed
`WTFStringImpl` storage for Latin-1 strings) and then hit JSC safepoints
before handing those pointers to `bsd_sendmmsg`:
- **`sendMany`**: subsequent loop iterations call `iter.next()` (slow
path → `JSObject.getIndex`), `coerceToInt32` on the port, and
`toBunString` on the address
- **`send`**: `parseAddr` calls `coerceToInt32` on the port and
`toBunString` on the address after the payload is captured
Any of these can run user JS that detaches an earlier payload's
ArrayBuffer via `.transfer(newLen)` (which synchronously frees the old
backing store) or drops the last reference to a JSString, leaving the
captured pointer dangling.
## Repro
```js
const buf = new ArrayBuffer(4096);
const payload = new Uint8Array(buf);
const evilPort = {
 valueOf() {
 buf.transfer(0); // synchronously frees the 4096-byte backing store
 return server.port;
 },
};
client.sendMany([payload, evilPort, "127.0.0.1"]); // or client.send(payload, evilPort, "127.0.0.1")
// bsd_sendmmsg reads 4096 bytes from the freed region
```
Under ASAN (with `Malloc=1` so bmalloc routes through the system heap):
```
==...==ERROR: AddressSanitizer: heap-use-after-free on address ... at pc ...
READ of size 4096 at ... thread T0
 #0 ... in read_iovec(...)
 #2 ... in sendmmsg
 #3 ... in bsd_sendmmsg packages/bun-usockets/src/bsd.c:123
freed by thread T0 here:
 ...
 oven-sh#14 ... in JSC::arrayBufferCopyAndDetach(...) JSArrayBufferPrototype.cpp:365
 ...
 oven-sh#30 ... in JSC::JSValue::toInt32(...) ← parseAddr's coerceToInt32
```
## Fix
- **`sendMany`**: root every payload JSValue in a `MarkedArgumentBuffer`
for the duration of the call and split the loop into two phases. Phase 1
collects/validates payload JSValues and runs all user-JS re-entrance
(`iter.next`, `parseAddr`). Phase 2 borrows byte slices from the rooted
JSValues once no more user JS sits between capture and `socket.send`. GC
cannot collect a rooted payload; an ArrayBuffer that was detached during
phase 1 reports a zero-length slice instead of a dangling pointer. No
payload bytes are copied.
- **`send`**: reorder so `parseAddr` runs before the payload pointer is
captured. `payload_arg` stays rooted in the callframe, and nothing
between capture and `socket.send` hits a JSC safepoint — so no copy is
needed.
## Verification
- **Without fix:** `bun bd test test/js/bun/udp/udp_socket.test.ts -t
'detaching an ArrayBuffer'` → ASAN heap-use-after-free in `read_iovec` →
`bsd_sendmmsg` for both `send` and `sendMany`, tests fail
- **With fix:** both tests pass; received bytes match the original
payload
- Full `test/js/bun/udp/` suite (207 tests) passes
- `zig:check-all` passes on all targets
---------
Co-authored-by: robobun <robobun@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
## Problem
`MarkedArrayBuffer.destroy()` did two things:
```zig
allocator.free(content.buffer.slice()); // free the bytes
allocator.destroy(this); // free *this
```
Every constructor that is actually used (`fromString`, `fromBytes`,
`fromJS`, `fromTypedArray`, `fromArrayBuffer`) returns
`MarkedArrayBuffer` **by value**, so `this` is never an individually
heap-allocated struct — it's a stack local, an embedded field, or an
ArrayList slot. The `allocator.destroy(this)` call passes that interior
pointer to mimalloc.
In the readdir Buffer error-cleanup path (`readdirWithEntries` /
`readdirInner`), entries are appended by value via
`Buffer.fromString()`:
- `allocator.destroy(&entries.items[0])` frees `entries.items.ptr`
- the next loop iteration reads `this.*` from poisoned memory
- `entries.deinit()` frees the same pointer again
## Repro
```js
const fs = require('fs');
// dir contains regular files + a self-referential symlink 'loop -> loop'
fs.readdirSync(dir, { encoding: 'buffer', recursive: true });
```
The recursive walk collects Buffer entries for the root, then fails with
`ELOOP` opening the symlink (not in the swallowed `NOENT/NOTDIR/PERM`
set), and enters the cleanup loop. Under ASAN:
```
==3593==ERROR: AddressSanitizer: use-after-poison on address 0x737ec6e50040
READ of size 64 at 0x737ec6e50040 thread T0
 #1 MarkedArrayBuffer.destroy array_buffer.zig:591
 #2 NodeFS.readdirInner node_fs.zig:5013
 #3 NodeFS.readdir node_fs.zig:4518
```
## Fix
- Drop `allocator.destroy(this)` from `MarkedArrayBuffer.destroy()`. The
struct is passed/stored by value; callers own its storage.
- Remove the unused `MarkedArrayBuffer.init()` (the only function that
heap-allocated the struct, zero callers) so there's no pairing that
would leak.
- The readdir call sites keep calling `.destroy()`, which still checks
`this.allocator` before freeing bytes — JS-owned buffers remain
untouched.
Also fixed the adjacent `Dirent` arm of the recursive-sync error
cleanup: `result.name.deref()` → `result.deref()` so `Dirent.path` is
released too (matching the non-recursive and async cleanup sites).
## Verification
New test in `test/js/node/fs/fs.test.ts` creates a temp dir with files +
a self-referential symlink, spawns a subprocess that calls
`readdirSync({encoding:'buffer', recursive:true})`, and asserts it
throws `ELOOP` and exits 0.
```
# without fix
(fail) readdirSync({encoding: 'buffer', recursive: true}) frees entries safely ...
 { exitCode: 134, stdout: "" } # SIGABRT from ASAN
# with fix
(pass) readdirSync({encoding: 'buffer', recursive: true}) frees entries safely ... [1.5s]
 { exitCode: 0, stdout: "ELOOP" }
```
`zig:check-all` passes on all targets.
---------
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
×ばつ ASAN ×ばつ CI). Apply the same multiplier to `expectMessage` / `expectReload` / `getStringMessage` / `getMostRecentHmrChunk` (all hardcoded 1000 ms), and raise the per-test base accordingly. Also make `waitForLine` scan already-buffered lines via the previously-dead `cursor` field so an `await` between stream creation and the call can't drop the match. ### Underlying DevServer bugs found while stress-testing - **`IncrementalGraph.invalidate` use-after-poison**: the incoming `path` (a slice into `HotReloadEvent.extra_files`) was stored in `entry_points`, but the event is reset — and its `extra_files` may be reallocated by the watcher thread — before `entry_points` is consumed by `startAsyncBundle` / `TestingBatch`. Since `getIndex(path)` already succeeded, store the graph-owned `keys[index]` instead. - **`TestingBatch.append`** stored the same borrowed slices as persistent keys across multiple `HotReloadEvent.run` calls. Dupe keys on insert; free them in `TestingBatch.deinit`. - **`onFileUpdate` (Linux)** indexed only `changed_files[event.name_off]` for a merged directory `WatchEvent`. When an atomic-save editor (vim/emacs/IntelliJ) lands `CREATE tmp` + `MOVED_TO target` in one coalesced inotify batch, the rename target was dropped and never re-watched. Forward every name via `event.names()`. ### Harness robustness - `waitForHotReload` used `clientWaits === connectedClients.size`; straggler HMR events from prior unsynchronized writes could push the count past, so it never matched. Use `>=`. - `waitForHotReload` now rejects on dev-server panic instead of hanging to the test timeout. - Detect `AddressSanitizer` / `ThreadSanitizer` / `==ABORTING` in subprocess output as a panic. ## How verified - New `hot.test.ts` case floods a watched directory (32 decoy creates + unlink + rename-over) to force inotify coalescing: - **without** `src/` changes → 3/3 fail under ASAN (use-after-poison in `TestingBatch.append` via `wyhash`) - **with** `src/` changes → 10/10 pass - `dev-and-prod.test.ts -t "rapid consecutive edits"` → 10/10 pass - Full runs of `hot`, `dev-and-prod`, `bundle`, `css`, `html`, `esm`, `stress`, `ssg-pages-router`, `incremental-graph-edge-deletion`, `plugins`, `sourcemap`, `server-sourcemap`, `vfile`, `framework-router`, `deinitialization` → all green (esm-11 is a pre-existing `skip: ["ci"]`) - `zig:check-all` passes on all targets Supersedes #29575 and #28211. Fixes #19732 --------- Co-authored-by: robobun <robobun@users.noreply.github.com>" data-pjax="true" href="/index.cgi/contrast/https://github.com/igorls/bun/commit/9bf6ea3312e3716eb26ff4186a527a51d8fc4cac">bake: fix entry_points UAF + inotify merged names; deflake test harne...
...ss (oven-sh#30181)
## What
Surveyed ~40 recent CI builds for bake test flakes and fixed the
underlying causes.
### Flake #1 (93 hits): `dev-and-prod-12: hmr handles rapid consecutive
edits`
Two modes:
- **Windows**: `Bun.write` is `open(O_TRUNC)` then async write with a
JS-thread round-trip in between. The watcher fires on the 0-byte
truncation, bundles an empty module that never calls `accept()`, and the
next update falls through to `fullReload()` → client exits
`unexpectedReload`.
- **All platforms**: after the final drain `client.messages.length = 0`,
a late hot_update lands during the following `await client.js\`...\`` and
trips the unread-messages disposal check.
**Fix (test):** use `fs.writeFileSync` for the rapid burst (microsecond
truncate window), write identical content so same-`sourceMapId`
duplicates are deterministic on every platform, and follow with a
synchronized sentinel write — once the sentinel arrives over the ordered
WS, every prior hot_update has been applied and nothing can leak into
disposal.
### Flake #2 (29 hits): `Timeout waiting for line "... socket connected"`
Across `react-spa`, `html`, `ssg-pages-router`, `bundle`, `hot`, `esm`,
`css`, `incremental-graph-edge-deletion`. `waitForLine()`'s default
timeout was **1 second** on non-Windows release builds — the Node client
has to start, import happy-dom, fetch, parse HTML, run the bundle, and
open a WebSocket in that window. The `ASAN_TIMEOUT_MULTIPLIER` constant
existed but was never applied.
**Fix (harness):** raise the base and apply a unified `WAIT_MULTIPLIER`
(debug ×ばつ ASAN ×ばつ CI). Apply the same multiplier to `expectMessage` /
`expectReload` / `getStringMessage` / `getMostRecentHmrChunk` (all
hardcoded 1000 ms), and raise the per-test base accordingly. Also make
`waitForLine` scan already-buffered lines via the previously-dead
`cursor` field so an `await` between stream creation and the call can't
drop the match.
### Underlying DevServer bugs found while stress-testing
- **`IncrementalGraph.invalidate` use-after-poison**: the incoming
`path` (a slice into `HotReloadEvent.extra_files`) was stored in
`entry_points`, but the event is reset — and its `extra_files` may be
reallocated by the watcher thread — before `entry_points` is consumed by
`startAsyncBundle` / `TestingBatch`. Since `getIndex(path)` already
succeeded, store the graph-owned `keys[index]` instead.
- **`TestingBatch.append`** stored the same borrowed slices as
persistent keys across multiple `HotReloadEvent.run` calls. Dupe keys on
insert; free them in `TestingBatch.deinit`.
- **`onFileUpdate` (Linux)** indexed only
`changed_files[event.name_off]` for a merged directory `WatchEvent`.
When an atomic-save editor (vim/emacs/IntelliJ) lands `CREATE tmp` +
`MOVED_TO target` in one coalesced inotify batch, the rename target was
dropped and never re-watched. Forward every name via `event.names()`.
### Harness robustness
- `waitForHotReload` used `clientWaits === connectedClients.size`;
straggler HMR events from prior unsynchronized writes could push the
count past, so it never matched. Use `>=`.
- `waitForHotReload` now rejects on dev-server panic instead of hanging
to the test timeout.
- Detect `AddressSanitizer` / `ThreadSanitizer` / `==ABORTING` in
subprocess output as a panic.
## How verified
- New `hot.test.ts` case floods a watched directory (32 decoy creates +
unlink + rename-over) to force inotify coalescing:
- **without** `src/` changes → 3/3 fail under ASAN (use-after-poison in
`TestingBatch.append` via `wyhash`)
 - **with** `src/` changes → 10/10 pass
- `dev-and-prod.test.ts -t "rapid consecutive edits"` → 10/10 pass
- Full runs of `hot`, `dev-and-prod`, `bundle`, `css`, `html`, `esm`,
`stress`, `ssg-pages-router`, `incremental-graph-edge-deletion`,
`plugins`, `sourcemap`, `server-sourcemap`, `vfile`, `framework-router`,
`deinitialization` → all green (esm-11 is a pre-existing `skip: ["ci"]`)
- `zig:check-all` passes on all targets
Supersedes oven-sh#29575 and oven-sh#28211.
Fixes oven-sh#19732
---------
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
...en-sh#30196)
## What does this PR do?
Fixes a use-after-free in `HTMLRewriter.transform()` that caused flaky
SIGSEGV crashes found by fuzzing.
When transforming a string or ArrayBuffer, the body is buffered
synchronously and fed to lol-html via `write()` followed by `end()`. If
a document/element handler returns a rejected promise for the final
`lastInTextNode` chunk (emitted from `end()`), the `end() catch` branch
in `BufferOutputSink.runOutputSink` would call `response.finalize()`
directly on the output `Response`.
That `Response` is already owned by its JS wrapper cell (created earlier
in `init()` via `sink.response.toJS()`), so destroying it in-place left
the wrapper's `m_ctx` pointing at freed memory. When GC later swept the
wrapper, its destructor invoked `Response.finalize()` again on that
freed pointer:
```
AddressSanitizer: use-after-poison
 #0 bun.js.bindings.JSRef.JSRef.deinit src/bun.js/bindings/JSRef.zig:188
 #1 bun.js.bindings.JSRef.JSRef.finalize src/bun.js/bindings/JSRef.zig:200
 #2 bun.js.webcore.Response.finalize src/bun.js/webcore/Response.zig:474
 #3 ResponseClass__finalize codegen/ZigGeneratedClasses.zig:17250
 #4 WebCore::JSResponse::~JSResponse() codegen/ZigGeneratedClasses.cpp:54979
```
The `write()` error path (just above it) already handled this correctly
by returning the error and letting the JS wrapper own the Response
lifetime. This PR makes the `end()` error path do the same — drop the
manual `response.finalize()` and `sink.response = undefined`.
## How did you verify your code works?
Minimal repro that reliably triggers the ASAN error before the fix and
passes cleanly after:
```js
const rewriter = new HTMLRewriter();
rewriter.onDocument({
 text(chunk) {
 if (chunk.lastInTextNode) {
 return Promise.reject(new Error("boom"));
 }
 },
});
try {
 rewriter.transform(new Uint8Array([97, 98, 99]).buffer);
} catch (e) {}
Bun.gc(true);
```
Added regression tests in `test/js/workerd/html-rewriter.test.js`
covering both ArrayBuffer and string inputs. All existing HTMLRewriter
tests pass.
---------
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
...0174)
## What
`RequestContext` stored `response_ptr: ?*Response` and, for plain
`Blob`/`InternalBlob`/`WTFStringImpl` bodies, left the Response JSValue
unprotected. `renderBytes()` → `tryEnd()` can hit backpressure and
register an `onWritable` callback, unwinding with `response_ptr` still
set. Nothing rooted the Response (`RequestContext` is a pool struct, not
GC-visited), so GC could finalize it. If the client then aborted while
the request body was still `.Locked`, `onAbort()` dereferenced a freed
`*Response` — heap-use-after-free under ASAN at
`RequestContext.zig:692`.
## Repro
```
POST → handler returns new Response(8MB string) sync
 → tryEnd() backpressure (client paused) → onWritable registered, return
 → Bun.gc(true) → Response collected, response_ptr dangles
 → client.destroy() → onAbort → deref response_ptr → UAF
```
ASAN trace (unpatched):
```
==ERROR: AddressSanitizer: use-after-poison
 #0 bun.js.bindings.JSRef.JSRef.tryGet
 #1 bun.js.webcore.Response.getBodyReadableStream
 #2 RequestContext.onAbort src/bun.js/api/server/RequestContext.zig:693
 #3 uWS::HttpContext<false>::onClose
```
## Fix
Give `Response` a `weak_ptr_data` field (mirroring `Request.WeakRef`)
and replace `response_ptr: ?*Response` with `response_weakref:
Response.WeakRef` via `bun.ptr.WeakPtr`. `Response.destroy()` now defers
freeing the allocation until outstanding weak refs drop; `WeakRef.get()`
returns null once the contents are gone.
`onAbort` / `handleResolveStream` / `handleRejectStream` call `.get()`
and simply skip the readable-stream cleanup when it's null — a no-op for
in-memory bodies anyway, since the body was already extracted via
`useAsAnyBlobAllowNonUTF8String()` before backpressure.
File-backed and `.Locked` bodies continue to `protect()`
`response_jsvalue` as before; those paths need the Response's
status/headers alive across the async hop for `renderMetadata()`. The
hot path (small in-memory responses) no longer needs
`protect()`/`unprotect()`.
The two redundant `ctx.response_ptr = response` assignments right before
`ctx.render(response)` are dropped — `render()` already sets the weak
ref.
## Verification
`test/js/bun/http/serve-response-gc-backpressure-abort.test.ts`
(ASAN/debug-only): POST with incomplete chunked body so `request_body`
stays `.Locked`, handler returns a large string Response, client pauses
so `tryEnd()` stalls, `Bun.gc(true)` loop, then client closes.
- **without fix**: `AddressSanitizer: use-after-poison` in `onAbort` →
`Response.getBodyReadableStream`
- **with fix**: passes, `abortCount === iterations`, `pendingRequests
=== 0`
---------
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
...before write (oven-sh#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:
 oven-sh#23 HTTPHeaderMap::remove
 oven-sh#28 doWriteHeaders RequestContext.zig:2303
 oven-sh#29 renderMetadata RequestContext.zig:2209
 oven-sh#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 oven-sh#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 oven-sh#15355` describe block continue to
pass.
Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 31, 2026
...s, http (oven-sh#30722)
Hardens 36 reachable security findings across the runtime, package
manager, parsers, HTTP client/server, and SQL drivers. Three
auto-applied fixes (oven-sh#61 SSL exception leak, oven-sh#68 YAML merge dedup, oven-sh#104
archive overwrite precheck) were dropped: oven-sh#61 introduced a
use-after-free, oven-sh#68 stored a non-`'static` byte view in a `'static`
field, and oven-sh#104 added dead gating that did not close the traversal.
### Memory safety / lifetime
- #2 — Dangling proxy slice across reentrant JS getter — copy
`process.env` proxy href to an owned `Vec` before reentrant getters can
free the env map (`Blob.rs`)
- oven-sh#15 — Rollback restores dangling editor name pointer — preserve and
restore `name_storage` on `detect_editor` failure (`BunObject.rs`)
- oven-sh#81 — Reentrant reconnect frees live handlers — only free previous
handlers when `active_connections == 0` (`Listener.rs`)
- oven-sh#110 — Async randomFill uses stale resizable buffer pointer — fill a
worker-owned scratch buffer; copy back on the JS thread after
re-validating bounds (`node_crypto_binding.rs`)
- oven-sh#119 — Null zero-length slice UB in DOMJIT fast path — use
`ffi::slice` which tolerates `(null, 0)` (`Crypto.rs`)
- oven-sh#67 — Raw serialization reads struct padding bytes — add explicit
`_padding_*` fields with `offset_of!` proof asserts (`npm.rs`)
- oven-sh#74 — TLS rejection path leaks websocket refcount — route SSL/auth
failures through `self.fail()` which clears `outgoing_websocket`
(`websocket_client.rs`)
- oven-sh#108 — FD-backed fetch body leaks duplicated descriptor — close
`opened_fd` unconditionally after `read_file` (`fetch.rs`)
### Untrusted-input bounds / panics
- oven-sh#10 — Invalid lockfile tag causes panic DoS — replace `unreachable!()`
with logged error + `Tag::Uninitialized` (`dependency.rs`)
- oven-sh#20 — Unchecked lockfile string offsets cause OOB slice — bounds-check
non-inline `String` pointers against `ctx.buffer` (`dependency.rs`)
- oven-sh#91 — Panic on unvalidated resolution tag — validate `ResolutionTag`
discriminants on lockfile load (`Package.rs`)
- oven-sh#24 — Unwrap panic on unexpected 304 response — return
`UnexpectedNotModified` when no cached manifest exists (`npm.rs`)
- oven-sh#44 — UDP port getter unwrap panic on transient state — return
`undefined` when `socket` is `None` (`udp_socket.rs`)
- oven-sh#36 — Close reason length mismatch causes panic — clamp `body_len` to
125 and bail on overlong UTF-8 transcode (`websocket_client.rs`)
- oven-sh#100 — Windows pipe name length panic DoS — `debug_assert` → real
bounds check (`Listener.rs`)
- oven-sh#60 / oven-sh#111 — Windows shim stack buffer overflows — bounds-check
argument and filename writes against `BUF1_LEN`/`BUF2_U16_LEN` before
`copy_nonoverlapping` (`bun_shim_impl.rs`)
- oven-sh#76 / oven-sh#101 — Unchecked bin name/entry name copies — bounds-check
before slicing into `abs_dest_buf` (`bin.rs`)
- oven-sh#79 — `if` keyword misclassification causes parser panic — require a
delimiter token before classifying (`shell_parser/parse.rs`)
- oven-sh#32 — Bounds check occurs after UTF-16 write — pre-flight key/value
lengths before `convert_utf8_to_utf16_in_buffer` (`env_loader.rs`)
- oven-sh#95 — PBKDF2 digest validation allows panic-only algorithm — reject
digests with no `EVP_MD` (`PBKDF2.rs`)
### DoS / resource caps
- oven-sh#17 — Unbounded recursion on deep TOML dotted keys — cap dotted-key
segments at 512 (`toml.rs`)
- oven-sh#39 — Unbounded brace expansion preallocation — cap expansion count at
65536 in `Bun.$` and `Bun.braces` (`BunObject.rs`, `Expansion.rs`)
- oven-sh#31 — SCRAM PBKDF2 parameters accepted from server — clamp iteration
count to `[4096, 10M]`, salt length to `[1, 1024]`
(`PostgresSQLConnection.rs`)
### Auth / injection / traversal
- oven-sh#19 — Cleartext password sent after TLS downgrade — require
`TLSStatus::SslOk`, not just `ssl_mode != Disable`
(`MySQLConnection.rs`)
- oven-sh#83 — Strict TLS request reuses lax-verified pooled socket — track
`established_with_reject_unauthorized` and refuse pool reuse for strict
callers (`HTTPContext.rs`, `lib.rs`, `ClientSession.rs`)
- oven-sh#73 — IPv6 loopback prefix auth bypass — exact-match `::1` instead of
`starts_with` (`server_body.rs`)
- oven-sh#56 — Unsanitized filename injects response headers — reject
`\r`/`\n`/NUL/`"` in `content-disposition` filenames
(`RequestContext.rs`)
- oven-sh#43 — Missing CRLF checks for signed host/auth headers — also validate
`region`, `access_key_id`, and `host` (`s3_signing/credentials.rs`)
- oven-sh#34 — Bucket slash enables S3 host confusion — reject buckets
containing `/` (`s3_signing/credentials.rs`)
- oven-sh#25 — Lexical symlink check permits extraction escape — track created
symlinks during extraction and refuse paths that traverse them
(`libarchive/lib.rs`)
- oven-sh#71 — bunx executes untrusted temp-cache binary — `lstat` cached
binary; refuse symlinks and other-uid files (`bunx_command.rs`)
### Permission hygiene
- #6 — Bin target chmod always sets mode 0777 — `0o777 & !umask` instead
of `umask | 0o777` (`bin.rs`)
- oven-sh#23 — Process umask cleared and never restored — restore umask after
probing it in `ensure_umask` (`bin.rs`)
### Parser correctness
- oven-sh#22 — Sign-prefixed scalar misparsed as infinity — fix Zig→Rust
`&&`/`||` precedence transliteration (`yaml.rs`)
igorls pushed a commit that referenced this pull request May 31, 2026
... stack pointer (oven-sh#31020)
## What
`resolveMaybeNeedsTrailingSlash` swaps `vm.log` / `resolver.log` to a
stack-local `Log` for the duration of `_resolve`, then restores them via
a drop guard. The Zig original also swaps and restores
`transpiler.linker.log` and `resolver.package_manager.log`; the Rust
port had those behind a `TODO(b2-cycle)` and only handled `vm.log` +
`resolver.log`.
When auto-install is enabled and the resolver lazily creates the
`PackageManager` during `_resolve`, `Resolver::get_package_manager`
seeds `pm.log` from `resolver.log` — which at that point is the
**stack-local** `Log`. Because the restore guard never touched `pm.log`,
it was left pointing into a dead stack frame after the function
returned. The next resolve at a different stack depth that routes
through the auto-install task runner dereferenced that stale pointer in
`Log::add_error_fmt`, tripping ASAN's `stack-use-after-scope` (or
segfaulting / executing garbage in release builds).
Stack at the fault:
```
#0 bun_ast::Log::add_formatted_msg
#1 bun_ast::Log::add_error_fmt
#2 bun_install::...::run_tasks
#7 bun_install::...::enqueue_dependency_to_root
#9 bun_resolver::Resolver::enqueue_dependency_to_resolve
oven-sh#14 bun_resolver::Resolver::resolve_and_auto_install
oven-sh#15 bun_jsc::VirtualMachine::_resolve
oven-sh#16 bun_jsc::VirtualMachine::resolve_maybe_needs_trailing_slash::<true>
```
## Fix
Swap and restore `linker.log` and (when present) `package_manager.log`
in both copies of the resolve log guard
(`VirtualMachine::resolve_maybe_needs_trailing_slash` and
`jsc_hooks::resolve_hook`), matching `VirtualMachine.zig`. The restore
re-checks `resolver.package_manager` at drop time so a PM that was
lazily created during `_resolve` is also pointed back at the VM log.
Also adds the missing `<cassert>` include in `wtf-bindings.cpp`, which
stopped being pulled in transitively.
## Repro
```js
// run from an empty dir with
// BUN_CONFIG_INSTALL=fallback BUN_CONFIG_REGISTRY=http://127.0.0.1:1
const realm = new ShadowRealm();
const variants = [
 () => realm.importValue("pkg-not-found-a", "x"),
 () => (() => realm.importValue("pkg-not-found-b", "x"))(),
 () => (() => (() => realm.importValue("pkg-not-found-c", "x"))())(),
 () => import("pkg-not-found-f"),
];
for (let i = 0; i < 100; i++)
 for (const v of variants) try { v()?.catch?.(() => {}); } catch {}
```
Segfaults on `main`, clean after this change.
Fixes oven-sh#14432
Fixes oven-sh#22407
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 31, 2026
...s longer than the comparand (oven-sh#31264)
### What does this PR do?
Fixes an ASAN `global-buffer-overflow` found by fuzzing the CSS parser:
```
asan:global-buffer-overflow:strncasecmp|eql_case_insensitive_ascii|eql_case_insensitive_ascii|bun_core::string::immutable::eql_case_insensitive_ascii_ignore_length
```
**Repro**
```sh
BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING=1 bun -e 'require("bun:internal-for-testing").cssInternals.minifyTest(":nth-child(Nn", "")'
```
```
==ERROR: AddressSanitizer: global-buffer-overflow READ of size 2 ...
 #0 strncasecmp
 #1 bun_core::strings_impl::eql_case_insensitive_ascii src/bun_core/lib.rs
 #2 bun_core::string::immutable::eql_case_insensitive_ascii_ignore_length src/bun_core/string/immutable.rs
 #3 bun_css::css_parser::nth::parse_nth src/css/css_parser.rs
 #4 bun_css::selectors::parser::parse_nth_pseudo_class src/css/selectors/parser.rs
```
**Cause**
`strings_impl::eql_case_insensitive_ascii(a, b, check_len)` defers to
`strncasecmp(a, b, a.len())`, which reads up to `a.len()` bytes from
*both* buffers. The Zig original (`strings.eqlCaseInsensitiveASCII`)
compared against NUL-terminated comptime literals, so `strncasecmp`
stopped at the sentinel and reported a mismatch whenever `a` was longer
than `b`. Rust byte-string literals carry no terminator, so the An+B
parser's ident branch (`parse_nth`), which compares an arbitrary user
ident against the keywords `"even" / "odd" / "n" / "-n" / "n-" / "-n-"`
with the ignore-length variant, reads past the end of the keyword
literal as soon as the ident is longer than the keyword and shares its
prefix (`Nn` vs `n`, `n-3` vs `n`, ...). Besides the OOB read, the
comparison result depended on whatever byte happens to follow the
literal in rodata.
**Fix**
Reject `b.len() < a.len()` up front in `eql_case_insensitive_ascii`
before calling `strncasecmp` — the same result the NUL sentinel produced
in Zig, so observable behavior is unchanged for every in-bounds input
(all other callers of the ignore-length variant already pass
equal-length slices). `strncasecmp` now only ever reads within both
slices.
**Verification**
- `bun bd test test/js/bun/css/nth-anplusb-ident.test.ts` without the
fix (src/ stashed): aborts with the ASAN global-buffer-overflow above.
- With the fix: passes. The new test covers valid `n-<digits>` idents
that are longer than the `n`/`n-` keywords (`:nth-child(n-3)`,
`:nth-child(N-3)`, `:nth-last-child(n- 42)`), keyword case-insensitivity
(`:nth-child(N)`), an invalid ident (`:nth-child(NN)` → parse error),
and the exact fuzzer-minimized input run in a subprocess.
- `bun bd test test/js/bun/css/css.test.ts`: 1032 pass, 0 fail (no
behavior change for the existing suite).
- A second fuzz report hits the same overflow through `Bun.build` with a
CSS entrypoint containing `:nth-child(Nn`; that path goes through the
same `parse_nth` comparison and is covered by this fix (`Bun.build` now
reports a parse error instead of aborting).
- The `build-rust` CI failures on this PR (unused label / unnecessary
`unsafe` warnings in `src/spawn`, `src/install`, `src/crash_handler`,
`src/runtime/ffi`, `src/runtime/dns_jsc`) are present on current `main`
commits that don't include this change and come from files this PR
doesn't touch.
@github-actions github-actions Bot changed the title (削除) deps: update libarchive to v3.8.7 (削除ここまで) (追記) deps: update libarchive to v3.8.8 (追記ここまで) Jun 28, 2026
@github-actions github-actions Bot changed the title (削除) deps: update libarchive to v3.8.8 (削除ここまで) (追記) deps: update libarchive to v3.8.9 (追記ここまで) Aug 2, 2026
igorls pushed a commit that referenced this pull request Aug 21, 2026
...ed (oven-sh#36247)
## What
`test/js/bun/http/bun-serve-html.test.ts` segfaults on `windows-aarch64`
after oven-sh#36175 landed (builds 84162, 84194; one earlier sighting in
83933):
```
panic(main thread): Segmentation fault at address 0x48
Features: ... dev_server(14) ...
```
Symbolicated in oven-sh#36214 as `AsyncFSTask<Access>::run_from_js_thread` with
`self = null`, i.e. a zeroed `ConcurrentTask` was dispatched.
## Cause
`DevServer.watcher_atomics.events[*].concurrent_task` is the intrusive
MPSC node the watcher thread links into `EventLoop.concurrent_tasks`
when it submits a hot-reload event. It was an inline field of
`DevServer`, so `server.stop()` → `drop(Box<DevServer>)` freed it while
it was still linked. The next `tick_concurrent` then read
`.next`/`.task`/`.auto_delete` from freed memory. ASAN on Linux
confirms:
```
heap-use-after-free: ConcurrentTask::get_next (unbounded_queue.rs)
 ← BatchIterator::next ← EventLoop::tick_concurrent_with_count
freed by: Box<DevServer>::drop ← NewServer::deinit_if_we_can
 ← NewServer::stop ← dispose_from_js (using server)
```
On release builds the freed block reads back as zeros, so the copied
`Task` is `{tag: 0, ptr: null}`; tag 0 is `task_tag::Access`, whose
`run_from_js_thread` loads `self.result` at offset `0x48`.
The bug is latent and platform-agnostic. oven-sh#36175 exposed it because the
CI runner now spawns the napi addon prebuild in the background while
serial tests run; that writes under the watched project root, so the
`jsx-runtime` DevServers in this test file now reliably receive a
hot-reload event between the last `await fetch` and `using server`
disposal.
## Fix
`watcher_atomics` is now a `NonNull<WatcherAtomics>` owned via
`bun_core::heap::into_raw`, so the allocation can outlive `DevServer`
and every queued pointer keeps allocation-root provenance.
`watcher_acquire_event`, `watcher_release_and_submit_event` and
`recycle_event_from_dev_server` take `*mut Self` and derive the returned
`*mut HotReloadEvent` (and the linked `concurrent_task` node) from that
root pointer via raw place projections rather than from a `&mut
WatcherAtomics` reborrow.
`Drop for DevServer` reads `next_event` after `Watcher::shutdown` has
serialised out the watcher thread (which guarantees it is stable):
- `DONE`: nothing is queued; clear and `heap::destroy` as before.
- otherwise: a `concurrent_task` is still linked (or its `Task` is
already in the drain FIFO). Null `owner` on every event and leave the
allocation alive.
`HotReloadEvent::run` checks `owner.is_null()` first; when set it
reclaims the allocation via the new `atomics` backref and returns
without touching the dead `DevServer`. The `# Safety` contracts on `run`
and the `BakeHotReloadEvent` dispatch arm are updated to describe the
null-owner case.
## Test
`test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts` creates a
development server, bundles once so `app.js` is watched, synchronously
rewrites `app.js`, spins briefly without yielding so the watcher thread
can enqueue, disposes the server, then yields. Ten iterations. In a
separate file because the React-bundling cases in
`bun-serve-html.test.ts` already exceed the default per-test timeout
under a debug+ASAN build on `main`.
<details><summary>fail-before (debug+ASAN, src/ at main)</summary>
```
==25521==ERROR: AddressSanitizer: heap-use-after-free on address 0x79315e4743e8
READ of size 8 at 0x79315e4743e8 thread T0
 #2 <ConcurrentTask as Node>::get_next unbounded_queue.rs:82
 #3 BatchIterator<ConcurrentTask>::next unbounded_queue.rs:135
 #4 EventLoop::tick_concurrent_with_count event_loop.rs:507
0x79315e4743e8 is located 488 bytes inside of 16512-byte region
freed by thread T0 here:
 #9 Box<DevServer>::drop
 oven-sh#12 NewServer<false,true>::deinit_if_we_can mod.rs:1770
 oven-sh#13 NewServer<false,true>::stop mod.rs:1665
 oven-sh#14 NewServer<false,true>::dispose_from_js server_body.rs:2584
```
</details>
Passes with the fix in ~2.4s under debug+ASAN (also on a local
`windows-aarch64` debug build, where the original
`bun-serve-html.test.ts` is now 19/19);
`test/bake/deinitialization.test.ts` still green.
Supersedes the producer half of oven-sh#36214 (which adds a sentinel for the
same zeroed-task symptom).
<!-- robobun:evidence:begin -->
---
**[review]** gate passed · iteration 4 · 6 files touched
<details><summary>fails on main (without fix)</summary>
```console
ASAN without fix: 1 failed, 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-html.test.ts test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts
bun test v1.4.0 (5f6622f)
test/js/bun/http/bun-serve-html.test.ts:
waitForServer /tmp/html-css-js_ObkyZk {
 "/": "/tmp/html-css-js_ObkyZk/index.html",
 "/dashboard": "/tmp/html-css-js_ObkyZk/dashboard.html",
}
[0.12ms] bundle index.html 1.09 KB
[0.05ms] bundle dashboard.html 1.27 KB
(pass) serve html [630.67ms]
waitForServer /tmp/bun-serve-html-txt_5C6B7a {
 "/": "/tmp/bun-serve-html-txt_5C6B7a/index.html",
}
[0.15ms] bundle index.html 0.40 KB
HASH efbnbska
(pass) serve plugins > basic plugin [556.20ms]
waitForServer /tmp/html-css-js-failing-plugin_OPRhwb {
 "/": "/tmp/html-css-js-failing-plugin_OPRhwb/index.html",
}
error: Plugin failed intentionally
 at /tmp/html-css-js-failing-plugin_OPRhwb/styles.css:0
error: Plugin failed intentionally
 at /tmp/html-css-js-failing-plugin_OPRhwb/styles.css:0
(pass) serve plugins > serve html with failing plugin [491.35ms]
waitForServer /tmp/html-css-js-empty-plugins_biqnN6 {
 "/": "/tmp/htm
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (96ff7ec)
test/js/bun/http/bun-serve-html.test.ts:
waitForServer /tmp/html-css-js_ZmgkBG {
 "/": "/tmp/html-css-js_ZmgkBG/index.html",
 "/dashboard": "/tmp/html-css-js_ZmgkBG/dashboard.html",
}
[0.00ms] bundle index.html 1.09 KB
[0.00ms] bundle dashboard.html 1.27 KB
(pass) serve html [25.17ms]
waitForServer /tmp/bun-serve-html-txt_uWNogT {
 "/": "/tmp/bun-serve-html-txt_uWNogT/index.html",
}
[0.00ms] bundle index.html 0.40 KB
HASH efbnbska
(pass) serve plugins > basic plugin [17.25ms]
waitForServer /tmp/html-css-js-failing-plugin_Kd1a8p {
 "/": "/tmp/html-css-js-failing-plugin_Kd1a8p/index.html",
}
error: Plugin failed intentionally
 at /tmp/html-css-js-failing-plugin_Kd1a8p/styles.css:0
error: Plugin failed intentionally
 at /tmp/html-css-js-failing-plugin_Kd1a8p/styles.css:0
(pass) serve plugins > serve html with failing plugin [16.33ms]
waitForServer /tmp/html-css-js-empty-plugins_Ecz7qJ {
 "/": "/tmp/html-css-js-empty-plugins_Ecz7qJ/index.html",
}
[0.00ms] bundle index.html 0.71 KB
(pass) serve plugins > empty plugin array [13.23ms]
Waiting for server
waitForServer /tmp/html-css-js-concurrent-plugins_l7wQg5 {
 "/": "/tmp/
... (truncated)
```
</details>
<details><summary>passes on PR (with fix)</summary>
```console
ASAN with fix: 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-html.test.ts test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts
bun test v1.4.0 (5f6622f)
test/js/bun/http/bun-serve-html.test.ts:
waitForServer /tmp/html-css-js_CpXhx4 {
 "/": "/tmp/html-css-js_CpXhx4/index.html",
 "/dashboard": "/tmp/html-css-js_CpXhx4/dashboard.html",
}
[0.09ms] bundle index.html 1.09 KB
[0.05ms] bundle dashboard.html 1.27 KB
(pass) serve html [588.64ms]
waitForServer /tmp/bun-serve-html-txt_rjZL4e {
 "/": "/tmp/bun-serve-html-txt_rjZL4e/index.html",
}
[0.15ms] bundle index.html 0.40 KB
HASH efbnbska
(pass) serve plugins > basic plugin [552.11ms]
waitForServer /tmp/html-css-js-failing-plugin_D8NJ2O {
 "/": "/tmp/html-css-js-failing-plugin_D8NJ2O/index.html",
}
error: Plugin failed intentionally
 at /tmp/html-css-js-failing-plugin_D8NJ2O/styles.css:0
error: Plugin failed intentionally
 at /tmp/html-css-js-failing-plugin_D8NJ2O/styles.css:0
(pass) serve plugins > serve html with failing plugin [505.55ms]
waitForServer /tmp/html-css-js-empty-plugins_vK0DVI {
 "/": "/tmp/htm
... (truncated)
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 673ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/7] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[2/7] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
 nightly-2026年07月20日-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026年07月19日)
�[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m Compiling�[0m
... (truncated)
```
</details>
<details><summary>diff hotspot</summary>
```
src/runtime/bake/DevServer.rs | 63 +++-
 src/runtime/bake/dev_server/lifecycle.rs | 12 +-
 src/runtime/bake/dev_server/mod.rs | 401 +++++++++++----------
 src/runtime/dispatch.rs | 10 +-
 .../http/bun-serve-html-hot-reload-drop.test.ts | 82 +++++
 test/js/bun/http/bun-serve-html.test.ts | 10 +-
 6 files changed, 374 insertions(+), 204 deletions(-)
```
</details>
**gate history** · 3 passed · 2 rejected · iteration 4
<details><summary>evidence per changed file</summary>
```
file reads edits tests
src/runtime/bake/DevServer.rs 10 13 0
src/runtime/bake/dev_server/lifecycle.rs 5 9 0
src/runtime/bake/dev_server/mod.rs 13 12 0
src/runtime/dispatch.rs 3 2 0
test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts 1 5 0
test/js/bun/http/bun-serve-html.test.ts 6 9 0
```
</details>
<!-- robobun:evidence:end -->
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
igorls pushed a commit that referenced this pull request Aug 21, 2026
×ばつ `yes`, 2 GB `dd`): with the old timer the listen block failed 1/5 runs at `function should not have been called`; with this change 5/5 runs pass under the same load (including a 246 ms `'listening'` that would have tripped the old 100 ms timer). `node-tls-server.test.ts` has the same 100 ms pattern and is also a serial `excludeFiles` entry; happy to fold it in here if preferred, but it hasn't been observed red so I kept this scoped to the reported file. <!-- robobun:evidence:begin --> --- **[stamp-90s]** gate passed · iteration 1 · 1 files touched <details><summary>passes on PR (with fix)</summary> ```console Test-only change. Debug/ASAN (expected pass): $ bun bd test 'test/js/node/net/node-net-server.test.ts' $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/node/net/node-net-server.test.ts bun test v1.4.0 (6b920f8b9) test/js/node/net/node-net-server.test.ts: (pass) net.createServer listen > should throw when no port or path when using options [27.65ms] (pass) net.createServer listen > should listen on IPv6 by default [153.42ms] (pass) net.createServer listen > should listen on IPv4 [26.37ms] (pass) net.createServer listen > should call listening [19.34ms] (pass) net.createServer listen > should provide listening property [22.93ms] (pass) net.createServer listen > should listen on localhost [17.90ms] (pass) net.createServer listen > should listen on localhost [17.45ms] (pass) net.createServer listen > should listen without port or host [24.19ms] (pass) net.createServer listen > should listen on unix domain socket [19.40ms] (pass) net.createServer listen > should bind IPv4 0.0.0.0 when listen on 0.0.0.0, issue#7355 [31.32ms] (pass) net.createServer events > should receive data [159.16ms] (pass) net.createServer events > should call end [155.39ms] (pass) net.createServer events > should call close [19.65ms] (pass) net.createServer events > should call connection and drop [70.90ms] (pass) net.createServer events > should error on an invalid port [23.08ms] (pass) net.createServer events > should call abort with signal [25.98ms] (pass) net.createServer events > should echo data [111.91ms] (pass) net.createServer events > #8374 [72.98ms] (pass) accepted socket event-loop hold matches Node (per-connection KeepAlive) > server.stop() + accepted socket.unref() lets the process exit [318.84ms] (pass) accepted socket event-loop hold matches Node (per-connection KeepAlive) > server.unref() alone does not drop a ref'd accepted connection's hold [1548.16ms] (pass) accepted socket event-loop hold matches Node (per-connection KeepAlive) > half-open accepted sockets after peer FIN do not busy-poll the event loop (Windows AFD DISCONNECT) [4 ... (truncated) Exit: 0 ``` </details> <details><summary>diff hotspot</summary> ``` test/js/node/net/node-net-server.test.ts | 117 +++++++------------------------ 1 file changed, 25 insertions(+), 92 deletions(-) ``` </details> **gate history** · 2 passed · 0 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests test/js/node/net/node-net-server.test.ts 2 3 0 ``` </details> <!-- robobun:evidence:end -->" data-pjax="true" href="/index.cgi/contrast/https://github.com/igorls/bun/commit/ccc6110ea09bff2ef606fc50c23d6cada5fd7cb4">test(net): drop 100ms setTimeout race from the net.Server listen tests (
oven-sh#36310)
### What does this PR do?
Fixes `test/js/node/net/node-net-server.test.ts > should listen on unix
domain socket` going red on the alpine 3.23 lanes since oven-sh#36175 (seen on
main builds 84293, 84503, 84549, 84601 and ~60 branch builds).
Every test in the `net.createServer listen` block armed
`setTimeout(closeAndFail, 100)` next to `server.listen()`. That timer
was never testing `listen()` itself: `Bun.listen` binds synchronously
and `'listening'` is scheduled via `setTimeout(emitListeningNextTick, 1,
this)`, so the 100 ms race was against the test process's own scheduling
latency. The runner already bounds each test, so the extra timer only
added a flake surface (and hid the real error behind `function should
not have been called`).
oven-sh#36175 didn't touch `net` or this file, but it moved the allowlisted
files into a single batch, so the handful of remaining serial files
(this one is in `excludeFiles`) now run much earlier in the shard. On
alpine that lands while the docker-service coordinator is still bringing
up the mysql containers in the background:
```
t=233226 [9/282] node-net-server.test.ts
t=233436 ✗ should listen on unix domain socket [144.58ms] ← 100 ms timer fired
t=234907 coordinator: mysql_native_password ready ← docker init finished 1.5 s later
t=242361 [attempt #2] node-net-server.test.ts 21 pass ← same file green once docker is idle
```
(from build 84601, alpine 3.23 x64 shard `019fab6d-27b7-4c39`)
### Change
Remove the 100 ms `setTimeout(closeAndFail, ...)` from the nine listen
tests and route `server.on('error', ...)` to `done(err)` so a real bind
failure reports its actual error. Same assertions, same code paths
(`listen()` → `'listening'` → `server.address()` checks); only the
hand-rolled deadline that duplicated the test runner's timeout is gone.
The 500 ms timers in the `events` block are untouched; they guard real
client↔server round trips, have `is_done` guards, and haven't flaked.
### How did you verify your code works?
- `bun bd test test/js/node/net/node-net-server.test.ts` → 21 pass / 0
fail.
- Reproduced the race locally by running the file under background
CPU+disk load (×ばつ `yes`, 2 GB `dd`): with the old timer the listen block
failed 1/5 runs at `function should not have been called`; with this
change 5/5 runs pass under the same load (including a 246 ms
`'listening'` that would have tripped the old 100 ms timer).
`node-tls-server.test.ts` has the same 100 ms pattern and is also a
serial `excludeFiles` entry; happy to fold it in here if preferred, but
it hasn't been observed red so I kept this scoped to the reported file.
<!-- robobun:evidence:begin -->
---
**[stamp-90s]** gate passed · iteration 1 · 1 files touched
<details><summary>passes on PR (with fix)</summary>
```console
Test-only change.
Debug/ASAN (expected pass):
$ bun bd test 'test/js/node/net/node-net-server.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/node/net/node-net-server.test.ts
bun test v1.4.0 (6b920f8)
test/js/node/net/node-net-server.test.ts:
(pass) net.createServer listen > should throw when no port or path when using options [27.65ms]
(pass) net.createServer listen > should listen on IPv6 by default [153.42ms]
(pass) net.createServer listen > should listen on IPv4 [26.37ms]
(pass) net.createServer listen > should call listening [19.34ms]
(pass) net.createServer listen > should provide listening property [22.93ms]
(pass) net.createServer listen > should listen on localhost [17.90ms]
(pass) net.createServer listen > should listen on localhost [17.45ms]
(pass) net.createServer listen > should listen without port or host [24.19ms]
(pass) net.createServer listen > should listen on unix domain socket [19.40ms]
(pass) net.createServer listen > should bind IPv4 0.0.0.0 when listen on 0.0.0.0, issue#7355 [31.32ms]
(pass) net.createServer events > should receive data [159.16ms]
(pass) net.createServer events > should call end [155.39ms]
(pass) net.createServer events > should call close [19.65ms]
(pass) net.createServer events > should call connection and drop [70.90ms]
(pass) net.createServer events > should error on an invalid port [23.08ms]
(pass) net.createServer events > should call abort with signal [25.98ms]
(pass) net.createServer events > should echo data [111.91ms]
(pass) net.createServer events > oven-sh#8374 [72.98ms]
(pass) accepted socket event-loop hold matches Node (per-connection KeepAlive) > server.stop() + accepted socket.unref() lets the process exit [318.84ms]
(pass) accepted socket event-loop hold matches Node (per-connection KeepAlive) > server.unref() alone does not drop a ref'd accepted connection's hold [1548.16ms]
(pass) accepted socket event-loop hold matches Node (per-connection KeepAlive) > half-open accepted sockets after peer FIN do not busy-poll the event loop (Windows AFD DISCONNECT) [4
... (truncated)
Exit: 0
```
</details>
<details><summary>diff hotspot</summary>
```
test/js/node/net/node-net-server.test.ts | 117 +++++++------------------------
 1 file changed, 25 insertions(+), 92 deletions(-)
```
</details>
**gate history** · 2 passed · 0 rejected · iteration 1
<details><summary>evidence per changed file</summary>
```
file reads edits tests
test/js/node/net/node-net-server.test.ts 2 3 0
```
</details>
<!-- robobun:evidence:end -->
igorls pushed a commit that referenced this pull request Aug 21, 2026
)
## What
`JSSink::assign_to_stream` now detaches the freshly created
`JSReadable*SinkController` (nulling its `m_sinkPtr`) when the C++
stream-pump setup returns an error, before returning to the caller.
## Why
The generated `${name}__assignToStream` functions create the controller
with `m_sinkPtr = sinkPtr` and then call into
`GlobalObject::assignToStream` → `readDirectStream` /
`readStreamIntoSink`. If that setup throws (for example a direct
`ReadableStream` whose `pull` getter throws), the controller is never
started, so nothing ever calls `end()`/`close()` to null `m_sinkPtr`.
The caller's error path (`Writable::init` for `Bun.spawn`) then releases
and frees the native sink. When the controller is later swept, its
destructor runs `${name}__controllerDetached` / `${name}__finalize` on
freed memory.
ASAN report:
```
heap-use-after-free on address 0x799feed81c78
READ of size 1
 #0 JSSink<FileSink>::js_controller_detached Sink.rs:567
 #1 FileSink__controllerDetached generated_jssink.rs:179
 #2 JSReadableFileSinkController::~JSReadableFileSinkController()
freed by:
 oven-sh#12 FileSink::deinit FileSink.rs:1142
 oven-sh#16 Writable::pipe_release Writable.rs:70
 oven-sh#17 Writable::init Writable.rs:339
 oven-sh#18 spawn_maybe_sync js_bun_spawn_bindings.rs:1379
```
The fix is at the generic `JSSink::assign_to_stream` layer so it covers
every sink type (`FileSink`, `NetworkSink`, `FetchRequestBodySink`,
...), not just the spawn path.
## Repro
```js
const { openSync, closeSync } = require("node:fs");
const fd = openSync("/tmp/out.txt", "w");
let armed = false;
const stream = new ReadableStream({
 type: "direct",
 get pull() { if (armed) throw new Error("pull unavailable"); return () => {}; },
});
armed = true;
try {
 Bun.spawn({ cmd: [process.execPath, "-e", "0"], stdio: [stream, fd, "ignore"] });
} catch {}
closeSync(fd);
Bun.gc(true); // sweep -> controller dtor -> UAF
```
## Tests
The two existing `spawn.test.ts` cases that cover the
stdin-stream-setup-throws path now force a full GC in the child fixture
so the controller destructor runs deterministically under debug+ASAN as
well. Previously they were only failing on the release-asan lane (where
the whole file has been quarantined as `[ASAN] [TIMEOUT]`), which is why
this went unnoticed.
```
bun bd test test/js/bun/spawn/spawn.test.ts -t "stdin stream setup fails"
```
fails on `main` (ASAN heap-use-after-free in the child's stderr) and
passes with this change.
`spawn-stdin-readable-stream-edge-cases.test.ts` and
`body-stream.test.ts` continue to pass.
igorls pushed a commit that referenced this pull request Aug 21, 2026
... sink ends inline (oven-sh#36939)
### Crash
Sentry [BUN-3BZF](https://bun-p9.sentry.io/issues/?query=BUN-3BZF)
(2,975 events since 2026年05月25日, macOS-dominant): `Panic: called
Option::unwrap() on a None value` at `FetchTasklet::callback`'s
`task_ref.http.as_mut().unwrap()`, reached from the HTTP thread's result
dispatch (`us_internal_ssl_on_data -> HTTPClient::fail ->
dispatch_result_and_reset -> AsyncHTTP::on_async_http_callback_raw ->
FetchTasklet::callback`). `http` is set once at creation and cleared
only at deinit, so the panic means the callback ran against a freed
`FetchTasklet`.
### Cause
`start_request_stream` takes a `+1` on the tasklet that must be released
exactly once by `write_end_request`. For a native `ByteStream` request
body (an upstream response body piped into `fetch()`),
`wire_native_sink` installs the sink's `source` handle *before* any of
its `EndedInline` returns (`ReadableStream.rs:328` vs `:337/:352/:359`),
so a stream that picked up an error or its last chunk between `fetch()`
and the `can_stream` tick comes back `EndedInline` with a native source
attached.
The `EndedInline` arm released the `+1` (via `write_end_request`) but
left `self.sink` installed with `ended == false`. Every terminal path
then runs `cancel_request_body_sink`, which saw a "live" native sink and
took its native arm: `abort_task()` plus a second `write_end_request` —
releasing the same `+1` again.
The double release collapses the refcount while the other owners (the
JS-side initial ref and the HTTP thread's in-flight ref) still use the
tasklet. Under ASAN the deterministic form is the trace below (deinit
runs inside `cancel_request_body_sink`, then `on_progress_update` keeps
using `self`). In release builds the same imbalance frees the tasklet
while it is still in use (or double-frees, handing a live tasklet's
block back to the allocator), which surfaces as downstream crashes in
the fetch completion path — the BUN-3BZF unwrap is the tasklet's `http`
field read from freed/recycled memory.
```
READ of size 8 ... core::mem::replace::<bun_jsc::js_promise::Strong>
 #2 FetchTasklet::on_progress_update FetchTasklet.rs:1158
freed by thread T0 here:
 oven-sh#12 FetchTasklet::deinit FetchTasklet.rs:509
 oven-sh#16 FetchTasklet::write_end_request FetchTasklet.rs:2281
 oven-sh#17 FetchTasklet::cancel_request_body_sink FetchTasklet.rs:2368
 oven-sh#18 FetchTasklet::on_progress_update FetchTasklet.rs:1143
```
### Fix
Leave the sink in the same state `end_from_stream` (the normal native
termination) leaves it: `ended = true`, source and task detached. The
terminal `cancel_request_body_sink` then hits its existing `if
sink.ended { return }` guard and cannot release the ref a second time
(it also no longer spuriously aborts a request whose body simply ended
inline).
### Verification
- New fixture `fetch-stream-body-ended-inline-fixture.ts` drives the
window: an upstream server that advertises a larger `content-length`
than it sends and closes a few ms later, piped as the body of a TLS
`fetch()` (the handshake keeps the wire-attempt window open), 100
iterations.
- Unfixed debug+ASAN build: heap-use-after-free with the trace above,
8/8 runs.
- Fixed build: `bun bd test
test/js/web/fetch/fetch-abort-stream-body.test.ts` passes (5 pass, 1
pre-existing skip), including the new test.
- `test/js/web/fetch/body-stream.test.ts`: 9086 pass / 0 fail.
`fetch.test.ts` and `fetch.stream.test.ts`: identical pass/fail counts
to an unfixed baseline in the same container (the failures are
pre-existing network/timeout issues).
- The test is `skipIf(!isASAN)`: the release build corrupts silently, so
only sanitizer lanes can observe the failure.
igorls pushed a commit that referenced this pull request Aug 21, 2026
...e cache (oven-sh#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 oven-sh#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 oven-sh#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 oven-sh#32180: that is `bun test`'s own
finalizer-owned memory, while this cache deliberately survives VM
teardown, so oven-sh#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>
igorls pushed a commit that referenced this pull request Aug 21, 2026
...-comparison (oven-sh#37168)
### Problem
`Bun__deepEquals` has heap-use-after-free when a getter on a nested
object mutates one of the objects being compared. All entry points are
affected: `Bun.deepEquals`, `expect().toEqual` / `toStrictEqual`,
`assert.deepStrictEqual` / `deepEqual`, and `util.isDeepStrictEqual`.
```js
// Malloc=1 <bun-asan> repro.mjs
const p1 = {}, p2 = {};
for (let i = 0; i < 8; i++) { p1['k'+i] = i; p2['k'+i] = i; }
let f = 0;
p1.a = { get x() { if (!f++) for (let i = 0; i < 2000; i++) p1['n'+i] = i; return 1; } };
p2.a = { get x() { return 1; } };
p1.z = 1; p2.z = 1;
Bun.deepEquals(p1, p2, true);
```
ASAN (with `Malloc=1` so JSC's bmalloc routes through the system
allocator):
```
heap-use-after-free READ of size 8
 #0 CompactPropertyTableEntry::key() Structure.h
 #1 PropertyTable::forEachProperty
 #2 Structure::forEachProperty
 #3 Bun__deepEquals<...> bindings.cpp
freed by:
 PropertyTable::destroyIndexVector <- PropertyTable::rehash <- PropertyTable::add
 <- Structure::addNewPropertyTransition <- JSObject::putDirectInternal
```
### Cause
The object fast path walks the structure's `PropertyTable` with
`Structure::forEachProperty` and recurses into `Bun__deepEquals` from
inside the lambda. Comparing a nested value can run a user getter; if
that getter adds (or deletes) properties on the parent object, JSC takes
the shared table off the old structure and rehashes it, freeing the
index vector the outer walk is iterating. Every remaining sibling
property is then read from freed memory and its stale offset fed to
`getDirect()`. In release builds this shows up as a SEGV at a forged
address or a wrong verdict.
### Fix
Collect the (left, right) value pairs into a `MarkedArgumentBuffer`
under `forEachProperty` with no side effects, then run `sameValue` and
the recursive comparisons after the walk finishes. This is the same
shape as `Object.assign`'s fast path (snapshot under `forEachProperty`,
side-effectful work after). The buffer keeps the snapshotted values
visible to GC, so allocation churn in a getter cannot collect them
either. The reverse `o2` walk already did only direct structure reads
and now also completes before any user code can run.
Verdicts are unchanged for non-mutating comparisons (existing suites
pass); a comparison whose getter mutates the object now
deterministically compares the snapshot, which matches Node's behavior
for the repro above (`true`).
### Verification
- New test in `test/js/bun/bun-object/deep-equals.test.ts` (renamed from
`deep-equals.spec.ts` to match the test naming convention): spawns an
ASAN child with `Malloc=1` (bmalloc routed through the system allocator
so ASAN can see the freed table) covering same-structure,
mixed-structure, delete, right-side mutation, and GC-churn variants
across all entry points. Fails before the fix (ASAN heap-use-after-free
abort), passes after.
- `test/js/bun/bun-object/`, `test/js/node/assert/deep-equal.test.ts`,
`assert-typedarray-deepequal.test.ts`: 542 pass.
- `test/js/bun/test/expect.test.js`: 415 pass.
- `test/js/node/test/parallel/test-assert-deep-with-error.js`: 2 pass.
### Scope
The same pattern exists in `JSC__JSValue__forEachPropertyImpl` in this
file (the `Bun.inspect` / `console.log` property walk), where the
formatter callback can run a nested value's `inspect.custom` mid-walk.
Verified with ASAN to hit the same free/read pair. That is a
pre-existing bug in the console/inspect subsystem and is intentionally
excluded here; a follow-up fix for that site is in progress. The other
`forEachProperty` sites (CommonJS export enumeration, HTTP header
writing, the ordered/non-indexed iteration variants) run no user code
inside the walk.
<!-- robobun:evidence:begin -->
---
**no test proof** · iteration 3 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/bun/bun-object/deep-equals.test.ts
<!-- robobun:evidence:end -->
igorls pushed a commit that referenced this pull request Aug 21, 2026
...id-format (oven-sh#37169)
### Problem
`Bun.inspect` and `console.log` have a heap-use-after-free when
formatting a value runs user code that mutates the object being
formatted. The default-enabled
`Symbol.for("nodejs.util.inspect.custom")` hook on a nested value is
enough to trigger it:
```js
// Malloc=1 <bun-asan> repro.mjs
const p = {};
for (let i = 0; i < 8; i++) p['k'+i] = i;
let f = 0;
p.a = { [Symbol.for('nodejs.util.inspect.custom')]() {
 if (!f++) for (let i = 0; i < 256; i++) p['n'+i] = i;
 return 'a';
} };
p.z = 1;
console.log(Bun.inspect(p).length);
```
ASAN (with `Malloc=1` so JSC's bmalloc routes through the system
allocator):
```
heap-use-after-free READ of size 8
 #0 CompactPropertyTableEntry::key() Structure.h
 #1 PropertyTable::forEachProperty
 #2 Structure::forEachProperty
 #3 JSC__JSValue__forEachPropertyImpl bindings.cpp
freed by:
 PropertyTable::destroyIndexVector <- PropertyTable::rehash <- PropertyTable::add
 <- Structure::addNewPropertyTransition <- JSObject::putDirectInternal
```
### Cause
The fast path of `JSC__JSValue__forEachPropertyImpl` walks the
structure's `PropertyTable` with `Structure::forEachProperty` and
invokes the formatter callback from inside the walk. The callback
recursively formats the property value, which can run user code: a
nested value's `inspect.custom`, or a getter on a built-in subclass (for
example an overridden `Map.prototype.size`). If that code adds or
deletes properties on the parent object, JSC rehashes the shared table,
freeing the index vector the outer walk is iterating, and every
remaining entry is read from freed memory.
The fast-path guard only inspects the parent's structure, and a parent
with plain data properties passes it; the hostile hook lives on a nested
value. Same bug class as the deepEquals fix in oven-sh#37168, which
deliberately excluded this site.
### Fix
Collect the entries (key, attributes, direct value) under
`forEachProperty` with no side effects, then resolve remaining values
and invoke the callback on the snapshot after the walk finishes. The
values go in a `MarkedArgumentBuffer` so GC in a callback cannot collect
them; keys are retained as `Identifier`s. The snapshot is per structure
walk, so the prototype-chain restart loop still re-reads each
prototype's live structure.
Properties added to the object while it is being formatted are no longer
printed: the walk now reflects the object as it was when formatting
started. That matches Node, which collects the key list before
formatting values. The other `forEachProperty` sites are unaffected: the
non-indexed and ordered variants never take this fast path, and the
remaining callers run no user code in the callback.
### Verification
- New test in `test/js/bun/util/inspect.test.js` (ASAN-only, child
spawned with `Malloc=1`) covering: `inspect.custom` adding properties
via `Bun.inspect` and `console.log`, deleting properties, a `Map`
subclass `size` getter, the prototype fast-walk of an own-property-less
object, and GC churn inside the hook with object-valued siblings
formatted afterwards. Fails before the fix (ASAN abort, empty stdout),
passes after.
- `test/js/bun/util/inspect.test.js`: 74 pass. `test/js/bun/console/`:
85 pass, 1 skip.
- `inspect-error.test.js` minified-file snapshots and
`inspect-error-leak.test.js` fail identically with and without this diff
locally (pre-existing, unrelated to property enumeration).
igorls pushed a commit that referenced this pull request Aug 21, 2026
...ven-sh#37273)
### Problem
`H2FrameParser::handle_received_stream_id` creates a `Stream` box,
inserts it into the stream map, and then invokes the JS `streamStart`
callback directly via `callback.call` without arming the
`DispatchGuard`, while still holding the raw `*mut Stream`. Every other
JS dispatch site in the parser arms the guard, because `rewrite_read`
frees streams queued in `pending_engine_stream_closes` only at dispatch
depth 0.
JS reached from inside that callback (the `Http2Stream` constructor
calls `this.on("pause", ...)`, so a patched `EventEmitter.prototype.on`
runs there; the handler also calls back into native `rstStream` for
refused streams) can close the just-created stream, queueing its
deferred free, and then re-enter `parser.read()` at depth 0. The drain
frees the box, and the callback return path writes the stream context
through the dangling pointer:
```
==ERROR: AddressSanitizer: heap-use-after-free ...
 #1 <bun_runtime::api::h2_frame_parser_body::Stream>::set_context src/runtime/api/bun/h2_frame_parser.rs:2110
 #2 <...H2FrameParser>::handle_received_stream_id src/runtime/api/bun/h2_frame_parser.rs:5372
 #3 <...H2FrameParser>::get_next_stream src/runtime/api/bun/h2_frame_parser.rs:8335
freed by:
 oven-sh#12 <...H2FrameParser>::rewrite_read::{closure#3} src/runtime/api/bun/h2_frame_parser.rs:5804
```
The callers that keep dereferencing the returned pointer (`request()`,
`get_next_stream`, the engine HEADERS path) were exposed to the same
freed box.
### Fix
Arm `enter_dispatch` across the callback, matching the invariant
documented on `enter_dispatch` (every section that holds a `Stream`
pointer while user JS can run must arm the guard). With the guard armed,
the deferred-close drain cannot run while the callback executes, so the
pointer stays valid for `set_context` and for the callers.
Also skip the context install when the callback closed the stream:
`free_resources` already dropped its `sctx` root, and re-inserting one
afterwards would pin the dead JS stream object until the session dies.
This is the guard-arming fix for the pre-existing issue flagged during
review of oven-sh#37272 (that PR only removes dead code around it).
### Verification
New test in `test/js/node/http2/node-http2-streams-rehash.test.ts` (the
file covering this class of reentrancy bugs) reproduces the exact
sequence: close the new stream and re-enter `read()` from inside the
`streamStart` callback. Without the fix it fails on every build tier:
heap-use-after-free under the ASAN debug build, and on release builds
`getStreamContext(2)` throws "Invalid stream id" because the drain
already freed the entry inside the callback. With the fix the entry
survives the callback with no context installed (covering the
skip-install branch), and a follow-up depth-0 `read()` asserts the
deferred close then actually drains. Existing http2 suites
(`node-http2.test.js`, `h2-conformance.test.ts`, the staged h2 tests,
node's server-push parallel tests) pass with the change.
<!-- robobun:evidence:begin -->
---
**[review]** gate passed · iteration 1 · 2 files touched
<details><summary>fails on main (without fix)</summary>
```console
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/http2/node-http2-streams-rehash.test.ts"
bun test v1.4.0 (8f79562)
test/js/node/http2/node-http2-streams-rehash.test.ts:
(pass) session.request() from a stream 'timeout' listener during forEachStream does not UAF on hashmap rehash [3284.09ms]
(pass) http2 client request() does not hold *Stream across user-controlled options getters [6184.76ms]
198 | env: bunEnv,
199 | stdout: "pipe",
200 | stderr: "pipe",
201 | });
202 | const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
203 | expect({ stdout: stdout.trim(), exitCode, stderr }).toMatchObject({ stdout: "OK", exitCode: 0 });
 ^
error: expect(received).toMatchObject(expected)
 {
- "exitCode": 0,
- "stdout": "OK",
+ "exitCode": 1,
+ "stderr": 
+ "=================================================================
+ ==101685==ERROR: AddressSanitizer: heap-use-after-free on address 0x79be9bb005c0 at pc 0x00000e7ec22e bp 
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (7725ac8)
test/js/node/http2/node-http2-streams-rehash.test.ts:
(pass) session.request() from a stream 'timeout' listener during forEachStream does not UAF on hashmap rehash [163.99ms]
(pass) http2 client request() does not hold *Stream across user-controlled options getters [78.42ms]
(pass) closing the new stream and re-entering read() inside the streamStart callback does not UAF [31.29ms]
(pass) http2 client write callback that opens new streams during flushQueue does not UAF [49.40ms]
(pass) DeferredTaskQueue::run tolerates an on_auto_flush callback that unregisters itself and returns true [46.51ms]
 5 pass
 0 fail
 5 expect() calls
Ran 5 tests across 1 file. [513.00ms]
__F:0:S:0
```
</details>
<details><summary>passes on PR (with fix)</summary>
```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/http2/node-http2-streams-rehash.test.ts"
bun test v1.4.0 (8f79562)
test/js/node/http2/node-http2-streams-rehash.test.ts:
(pass) session.request() from a stream 'timeout' listener during forEachStream does not UAF on hashmap rehash [3278.34ms]
(pass) http2 client request() does not hold *Stream across user-controlled options getters [6171.66ms]
(pass) closing the new stream and re-entering read() inside the streamStart callback does not UAF [1906.06ms]
(pass) http2 client write callback that opens new streams during flushQueue does not UAF [2819.28ms]
(pass) DeferredTaskQueue::run tolerates an on_auto_flush callback that unregisters itself and returns true [2618.43ms]
 5 pass
 0 fail
 5 expect() calls
Ran 5 tests across 1 file. [19.19s]
__F:0:S:0
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 689ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 93 exports (host=3, lazy=10, generic=80, rust=0); 239 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
 nightly-2026年07月20日-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026年07月19日)
�[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m Compiling�[0m bun_brotli v
... (truncated)
```
</details>
<details><summary>diff hotspot</summary>
```
src/runtime/api/bun/h2_frame_parser.rs | 19 +++-
 .../node/http2/node-http2-streams-rehash.test.ts | 100 +++++++++++++++++++++
 2 files changed, 115 insertions(+), 4 deletions(-)
```
</details>
**gate history** · 2 passed · 0 rejected · iteration 1
<details><summary>evidence per changed file</summary>
```
file reads edits tests
src/runtime/api/bun/h2_frame_parser.rs 10 4 0
test/js/node/http2/node-http2-streams-rehash.test.ts 2 3 0
```
</details>
<!-- robobun:evidence:end -->
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
igorls pushed a commit that referenced this pull request Aug 21, 2026
...n-sh#37813)
### Problem
- An HTML route served without the DevServer (`development: false` or `{
hmr: false }`) bundles on its first request. If the only client
disconnects and `server.stop(true)` is called while a `[serve.static]`
plugin still has that build parked, `stop()` settles, the next GC frees
the server, and the build then finishes against the freed server.
- Debug build: `AddressSanitizer: heap-use-after-free` in
`html_bundle::Route::on_complete` (parked in `onLoad`) or
`Route::on_plugins_resolved` (parked in the plugin's `setup()`). A
release build reads the freed `NewServer` with no report.
- Cause: while a route is building, nothing counts as keeping the server
alive. The clients waiting on the build only count as connections, so
once they drop the server's idle check sees no pending work.
- The other route kinds already count their asynchronous work in the
server's pending-request counter; the HTML build was the one piece of
in-flight work that did not.
### Fix
- Entering the building state now takes one pending request on the
server; both ways out of it (build finished, plugin load rejected)
answer the waiting clients and then release it.
- This holds the server for exactly the window in which the route will
call back into it. The release runs the server's idle pass, so a server
stopped mid-build is freed right after the build lands.
- Visible change: `server.pendingRequests` is 1 while an HTML route
bundles and `await server.stop()` waits for the bundle, as it already
does for a `fetch` handler still running. A build cancelled by VM
teardown is not covered; at exit it leaves the same state an in-flight
`fetch` handler does.
- Verification: a new test parks the route in the build, in the plugin
load, and in a plugin load that rejects. Unfixed debug build: all three
report 0 pending requests and an early-settled `stop()`, and the first
two die with the ASAN reports above. Fixed: all three pass, as do the
existing HTML-serve tests that do not need the DevServer.
### Background
- HTML routes: `Bun.serve({ routes: { "/": html } })` with an imported
`.html` file. Without the DevServer the route bundles the page once, on
the first request, registers the outputs as static routes, and holds
requests that arrive during the build.
- `[serve.static]` plugins: a bunfig entry naming bundler plugins for
these routes, loaded on the first request. Both the plugin load and the
bundle finish on later event-loop turns and complete by calling back
into the server through a raw pointer stored on the route.
- Pending requests: the server's count of in-flight work, exposed as
`server.pendingRequests`. `stop()` settles and the server can be torn
down only when the count is zero; static and file routes already raise
it when a response goes asynchronous.
- Server lifetime: stopping a server does not free it. Once nothing is
pending, the JS wrapper becomes collectable and the native server is
freed on a later GC, so a stale pointer to it only fails after a GC.
<details>
<summary>Original description</summary>
### Repro
HTML route served without the DevServer (`development: false` or `{ hmr:
false }`), with a `[serve.static]` plugin whose `onLoad` parks on a
promise. Request the route, drop the client, `server.stop(true)`, drop
the server, `Bun.gc(true)` plus a couple of event-loop turns, then let
`onLoad` resolve. Debug (ASAN) build:
```
==1165==ERROR: AddressSanitizer: heap-use-after-free on address 0x73defa800738 ...
READ of size 8 at 0x73defa800738 thread T0
 #0 in <bun_runtime::server::NewServer<false, false>>::global_this src/runtime/server/mod.rs:451
 #1 in <bun_runtime::server::AnyServer>::global_this src/runtime/server/mod.rs:3847
 #2 in <bun_runtime::server::html_bundle::Route>::on_complete src/runtime/server/HTMLBundle.rs
 #3 in JSBundleCompletionTask::on_complete src/runtime/api/js_bundle_completion_task.rs:642
freed by thread T0 here:
 ...
 oven-sh#11 in <bun_runtime::server::NewServer<false, false>>::deinit src/runtime/server/mod.rs:2122
 oven-sh#12 in NewServer::schedule_deinit::{closure#1} src/runtime/server/mod.rs:1957
```
Parking in the plugin's `setup()` instead (so the route is still waiting
for the plugin load when the server goes away) gives the same report one
step earlier:
```
READ of size 1 ... in <bun_runtime::server::html_bundle::Route>::on_plugins_resolved src/runtime/server/HTMLBundle.rs
 #1 in <bun_runtime::server::server_body::ServePlugins>::handle_on_resolve src/runtime/server/server_body.rs:1150
 #2 in bun_runtime::server::server_body::on_resolve_impl
```
On a release build the same sequence reads a freed `NewServer` (its
config, then `append_static_route` / `reload_static_routes` on it)
without a report.
### Cause
`html_bundle::Route` keeps a raw `server` back-pointer and bundles on
its first request. Both the plugin load and the build finish on later
event-loop turns and call back into the server through that pointer
(`on_plugins_resolved` reads the config, `on_complete` registers the
output files as static routes and reloads the route table). While the
route is in `State::Building`, nothing holds the server on its behalf:
`on_plugins_resolved` only refs the route itself, and the clients
waiting in `pending_responses` only count as connections, which they can
drop at any time. So once the last client disconnects and the server is
stopped, `deinit_if_we_can` sees no pending requests, settles `stop()`,
downgrades the wrapper, and the next GC frees the `NewServer` with the
build still in flight.
`StaticRoute` / `FileRoute` / `DirectoryRoute` already handle their
asynchronous work with the server's `pending_requests` counter
(`on_pending_request` when a response goes async,
`on_static_request_complete` when it finishes); the route's build is the
same kind of in-flight work and was the one thing not counted.
### Fix
`schedule_bundle` calls `server.on_pending_request()` whenever the route
enters `State::Building` (plugins ready, or plugins still loading), and
the two ways out of that state (`on_complete`, `on_plugins_rejected`) go
through a new `finish_building`, which answers the pending responses and
then calls `on_request_complete()`. That keeps the server allocated for
exactly the window in which the route will call back into it, and
`on_request_complete` runs the idle pass, so a server that was stopped
while building is downgraded and freed right after the build lands (the
`stop()` promise now settles then as well, matching what happens for a
`fetch` handler that is still running when `stop()` is called). With
that invariant, `on_complete` no longer needs its `Option` handling of
the back-pointer; it takes the server once at the top, the same way
`on_plugins_resolved` already did.
A visible consequence: `server.pendingRequests` is 1 while an HTML route
is bundling, and `await server.stop()` waits for the bundle. A build
whose plugin never settles therefore keeps the server allocated, as an
unsettled `fetch` handler already does. Not covered: a build cancelled
by VM teardown never reaches `Route::on_complete` (the completion task
returns early on `cancelled`), so at exit the route keeps its ref and,
now, its pending request; that is the same state an in-flight `fetch`
handler leaves a server in at exit and nothing observes it. The
DevServer's own plugin wait uses a different back-pointer and is not
changed here.
### Verification
`test/js/bun/http/bun-serve-html-build-holds-server.test.ts` (separate
small file; `bun-serve-html.test.ts` is too slow under the debug ASAN
build for a lifetime test, as `bun-serve-html-hot-reload-drop.test.ts`
notes). One fixture, parked in turn in the build (`onLoad`), in the
plugin load (`setup()`), and in a plugin load that then rejects (the
`on_plugins_rejected` exit has to release the request too). Each child
reports `server.pendingRequests` while parked, whether `stop(true)`
settled across ten event-loop turns before the route was released, and
whether the wrapper became collectable afterwards; the test expects `{
pendingRequestsWhileParked: 1, stopBeforeRelease: "pending",
collectedAfterwards: true }` plus a clean exit. If `stop()` did settle
early, the fixture lets the server get collected before releasing the
route, which is the sequence above.
Unfixed debug build: all three report `pendingRequestsWhileParked: 0,
stopBeforeRelease: "settled"`, and the first two children die with the
ASAN reports above (the rejection case has no use-after-free to hit; it
fails on the report). Fixed: the three pass in under a second each. Also
run on the fixed debug build: `bun-serve-html-405.test.ts`,
`bun-serve-html-hot-reload-drop.test.ts`,
`test/bake/serve-plugins-dev-server.test.ts` (all pass), and
`bun-serve-html.test.ts`, where everything that does not need the
DevServer passes, including `serve plugins > concurrent requests to
multiple routes during plugin load`; its `development: true` cases fail
in this container with `EMFILE while initializing file watcher for
development server` (inotify instance limit) before reaching any of this
code.
</details>
igorls pushed a commit that referenced this pull request Aug 22, 2026
oven-sh#39947)
### Problem
- A worker whose entry point goes through a package.json `imports` or
`exports` map leaks 12 KiB (3 `PathBuffer`s, more on Windows) when its
thread exits. On an ASAN build LeakSanitizer reports `Direct leak of
12288 byte(s)` allocated in `module_bufs`
(`src/resolver/package_json.rs`), reached from
`resolve_entry_point_specifier` on the worker thread.
- Cause: `MODULE_BUFS` is a thread local `Cell<*mut ModuleBufs>` with
nothing that frees the box. The resolver's other per thread buffers
(`BufsSlot` in `resolver.rs`, `LazyPathBuf` in `bun_paths`) got a
destructor in oven-sh#30875. This one did not.
### Fix
- Wrap the pointer in `ModuleBufsSlot`, whose `Drop` destroys the box
when the thread exits. Same shape as `BufsSlot`. Access is unchanged, so
the recursion notes on the thread local still hold, and the static TLS
template is still one pointer.
- Correct because the destructor runs when the thread's TLS is torn
down, after every resolver frame on that thread has returned. The main
thread's box lives for the process, as before.
- Verified: `test/js/web/workers/worker-entry-point.test.ts` (new file)
runs a worker through an `imports` alias in a child with
`detect_leaks=1`. It fails on main with the report above and passes with
this change (checked both ways with a debug build).
`test/js/bun/binary/tls-segment-size.test.ts` still passes.
### Background
- The resolver keeps a few large scratch buffers per thread instead of
on the stack. They are boxed on first use and only a pointer sits in
TLS, so the TLS segment stays small on every platform.
- A worker thread resolves its own entry point and preloads, so it is
the common short lived thread that touches these buffers. The bundler's
pool threads live as long as the pool.
- The ASAN CI lanes run test children with `detect_leaks=1`. The test
sets that itself (plus the repo's `test/leaksan.supp`) so that a local
ASAN build checks it too. A build without ASAN ignores the options and
checks the behaviour only.
<details><summary>Notes</summary>
Found through oven-sh#39811, whose worker test resolves an `imports` alias and
failed on the ASAN lanes because of this leak. oven-sh#39811 carries this
change until this lands and is otherwise independent of it. oven-sh#35060
(overflow bundle threads, open) includes the same change as one of its
hunks, because its threads are short lived too.
The case is in its own file, for the worker entry point resolution
cases, rather than in `worker.test.ts`: three of that file's stress
cases go over their budget on a debug build on a slow machine, which
would hide whether this case itself flips. oven-sh#39811 adds its worker case
to the same file.
Without `print_suppressions=0` LeakSanitizer prints a "Suppressions
used" table to stderr on exit when an unrelated, suppressed allocation
exists in the process, so the test passes that along with the
suppressions file when the environment does not already set
`LSAN_OPTIONS`.
</details>
<!-- robobun:evidence:begin -->
---
**[review]** gate passed · iteration 1 · 2 files touched
<details><summary>fails on main (without fix)</summary>
```console
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/workers/worker-entry-point.test.ts
bun test v1.4.0 (4199361)
test/js/web/workers/worker-entry-point.test.ts:
41 | LSAN_OPTIONS:
42 | bunEnv.LSAN_OPTIONS ??
43 | `print_suppressions=0:suppressions=${path.join(import.meta.dir, "..", "..", "..", "leaksan.supp")}`,
44 | },
45 | );
46 | expect(stderr).toBe("");
 ^
error: expect(received).toBe(expected)
- ""
+ "
+ =================================================================
+ ==385090==ERROR: LeakSanitizer: detected memory leaks
+ 
+ Direct leak of 12288 byte(s) in 1 object(s) allocated from:
+ #0 0x000007dd95c8 in malloc crtstuff.c
+ #1 0x00000be19934 in std::sys::alloc::unix::alloc /root/.rustup/toolchains/nightly-2026年07月20日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/alloc/unix.rs:31:18
+ #2 0x00000be184b9 in <std::alloc::System>::alloc_impl /root/.rustup/toolchains/nightly-2026年07月20日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/alloc.rs:149:78
+ #3 
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (2e16ac4)
test/js/web/workers/worker-entry-point.test.ts:
(pass) package.json imports alias as the entry point > the worker runs and its thread exits without leaking [11.86ms]
 1 pass
 0 fail
 3 expect() calls
Ran 1 test across 1 file. [218.00ms]
__F:0:S:0
```
</details>
<details><summary>passes on PR (with fix)</summary>
```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/workers/worker-entry-point.test.ts
bun test v1.4.0 (4199361)
test/js/web/workers/worker-entry-point.test.ts:
(pass) package.json imports alias as the entry point > the worker runs and its thread exits without leaking [3743.31ms]
 1 pass
 0 fail
 3 expect() calls
Ran 1 test across 1 file. [6.05s]
__F:0:S:0
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 667ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
 nightly-2026年07月20日-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026年07月19日)
�[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
�[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
�[1m�[92m Compiling�[0m bun_outpu
... (truncated)
```
</details>
<details><summary>diff hotspot</summary>
```
src/resolver/package_json.rs | 23 +++++++++---
 test/js/web/workers/worker-entry-point.test.ts | 50 ++++++++++++++++++++++++++
 2 files changed, 68 insertions(+), 5 deletions(-)
```
</details>
**gate history** · 1 passed · 1 rejected · iteration 1
<details><summary>evidence per changed file</summary>
```
file reads edits tests
src/resolver/package_json.rs 2 2 0
test/js/web/workers/worker-entry-point.test.ts 1 2 0
```
</details>
**root cause** · written by the author bot
With --target bun or node, the resolver short-circuits node:, bun: and
hardcoded builtin specifiers into an external result whose primary path
is the bare specifier rather than an absolute file path, and entry-point
resolution passed that through, so enqueue_entry_item either tripped the
absolute-path assert, reported a misleading "File not found", or for
bun:wrap collided with the runtime's pre-registered source and left the
build with no entry points. The fix marks entry-point resolutions with
their own ImportKind so the resolver no longer applies externalization
rules to them, and resolv...
<!-- robobun:evidence:end -->
igorls pushed a commit that referenced this pull request Aug 24, 2026
...40064)
### Problem
- GitHub closes only the first reference after a keyword, so "Fixes #1,
#2" leaves #2 open. "Supersedes #3" links nothing, and no reference
closes a pull request.
- The last 1000 merged PRs name 274 such references. PR oven-sh#32292 is open
although merged oven-sh#36135 says "Supersedes oven-sh#32292".
### Fix
- `.github/workflows/close-linked-issues.yml` runs on
`pull_request_target` `closed` (a merge into the default branch of
`oven-sh/bun`) and on `workflow_dispatch` with a PR number and
`dry_run`. Everything is inline in one `actions/github-script` step,
with no checkout.
- Each open target is closed as `completed` with the comment "Closed as
completed by #N." or "Superseded by #N.". Closed or missing targets, the
PR itself and other repositories are skipped.
- The parser has no regex. A closing keyword (close, fix, resolve,
supersede, replace, any tense) must lead the reference, alone or in a
list. A negated, hedged or noun keyword, or one whose subject is another
reference, does not count ("may fix", "the rm fix #1", "oven-sh#100 supersedes
#1").
- Verified: `test/internal/close-linked-issues.test.ts` (333 cases) runs
the YAML's script against fake `github`, `context` and `core`. Also the
1000-PR parse (Notes).
### Background
- GitHub's own keywords are close, fix and resolve (-s, -ed). Each links
one reference, and only a merge into the default branch closes it.
- `pull_request_target` runs in the base repository with a write token,
also for fork PRs. That is safe only when no PR-controlled code runs.
Here the description is the only PR input, parsed as text.
<details><summary>Notes</summary>
A close through the API does not create the "closed this in #N" timeline
link that GitHub makes for its own closes. The comment carries the PR
number instead.
How the parser was calibrated. I pulled the descriptions of the last
1000 merged PRs and listed every line with a keyword next to a
reference. The keyword families, list shapes and reference forms in the
script are the ones that appear there. A reference is `#1`,
`owner/repo#1`, an issue or pull URL (bare or in `<>`), or a markdown
link. Four lines would have been wrong with a plain
keyword-then-reference rule, and each led to a rule:
- "the open `rm` fix oven-sh#37521" (oven-sh#38379): "fix" as a noun. Base forms (fix,
close, resolve, supersede, replace) count only at the start of a
sentence or line, or after will, should, does, and, and a few similar
words. "to" is not one of them ("unable to fix #1", "how to fix #1").
- "May also fix oven-sh#12318 / oven-sh#10046, untested" (oven-sh#38242): hedged. may, might,
could, would, partially and the negations disqualify the keyword,
looking past adverbs such as "also".
- "Supersedes the closed oven-sh#26040" (oven-sh#36289) and "a comment on closed
oven-sh#35351" (oven-sh#35365): "closed" as an adjective. A determiner or preposition
before the keyword disqualifies it.
- "supersedes oven-sh#33130's optimisation" (oven-sh#35843): a number that continues
into a word is not a reference.
Review added: a reference before the keyword is the subject ("oven-sh#100
supersedes #1"), also through "which" or "that" ("reverts oven-sh#100, which
fixed #1") and across a removed span ("oven-sh#100 ~~also~~ fixes #1"). A hedge
two words before the keyword disqualifies it ("hopefully this fixes #1",
"could this fix #1?"). A clause that starts with if, when, once, until
or unless is not a statement. The tokenizer keeps a line break as a
token so that "Fixes #1" on one line and "Fixes #2" on the next stay two
statements. Code spans, fences, indented code, blockquotes, HTML
comments and strikethrough are skipped. The block stripping follows
CommonMark for fences (also inside a blockquote), indented code,
blockquotes with lazy continuation, setext underlines and HTML comments,
and GFM for `~~` flanking.
Result over the 1000 descriptions: 274 distinct references in 135 PRs. I
checked the current state of all of them through GraphQL. All but one
are closed (202 issues completed, 5 duplicates, 66 pull requests). The
one open target is PR oven-sh#32292, superseded by merged oven-sh#36135. No open
target is a false positive. Every review change kept this result.
Patterns that are deliberately not handled: a bulleted list under
"Closes:" on its own line (not seen in the sample), references separated
by whitespace only ("#1 #2"), "fix for #1", and GH-1 style references. A
`?` after the list is not treated as a question. The block parser tracks
no list containers, so a second paragraph of a list item indented by
four spaces is read as an indented code block and skipped. A removed
span or inline comment reads as one word, so "Fixes <!-- n --> #1" finds
nothing.
The test suite covers: the phrases above, stopping at the right place in
real sentences, CRLF descriptions, URLs with fragments or a `/files`
suffix, case-insensitive `Owner/Repo#1`, the fake API where a lookup, an
update or a comment fails, the `dry_run` input, an invalid `pr_number`
input, an unmerged PR, a PR merged into a non-default branch, the merge
event body against a later edit, and a description with no closing
statement.
The first revision of this PR checked out the repository and ran
`scripts/close-linked-issues.ts`. Jarred asked for no checkout and no
script file, so the script moved inline into the workflow and the test
now reads it out of the YAML.
</details>
<!-- robobun:evidence:begin -->
---
**[stamp-90s]** gate passed · iteration 9 · 2 files touched
<details><summary>passes on PR (with fix)</summary>
```console
Test-only change.
Debug/ASAN (expected pass):
$ bun bd test 'test/internal/close-linked-issues.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/internal/close-linked-issues.test.ts
bun test v1.4.1 (4448a2e)
test/internal/close-linked-issues.test.ts:
(pass) finds "Fixes oven-sh#39852" [176.21ms]
(pass) finds "Closes oven-sh#31772. Fixes oven-sh#31771." [22.28ms]
(pass) finds "- Fixes oven-sh#39930" [12.28ms]
(pass) finds "Fixes: oven-sh#30429" [10.46ms]
(pass) finds "FIXES #1" [7.86ms]
(pass) finds "(Fixes #1)" [8.97ms]
(pass) finds "**Fixes #1**" [10.20ms]
(pass) finds "__Fixes #1__" [9.83ms]
(pass) finds "_Fixes #1_" [11.25ms]
(pass) finds "Fixes **#1**" [9.72ms]
(pass) finds "**Fixes** #1" [7.13ms]
(pass) finds "**Fixes:** #1" [8.11ms]
(pass) finds "Fixes #1 and **#2**" [11.47ms]
(pass) finds "Fixes **#1**, **#2**" [9.13ms]
(pass) finds "## Why (fixes oven-sh#13771, closes oven-sh#30543)" [16.08ms]
(pass) finds "Closes oven-sh#11418" [19.46ms]
(pass) finds "Resolves #1. Resolved #2. Resolve #3." [12.09ms]
(pass) finds "Fixes oven-sh#34055, oven-sh#30327, oven-sh#24394, oven-sh#20816, oven-sh#32403, oven-sh#11898, oven-sh#10056." [17.11ms]
(pass) finds "Fixes oven-sh#18192 and oven-sh#31675 as a consequence" [10.45ms]
(pass) finds "Fixes #1, #2, and #3" [10.96ms]
(pass) finds "Fixes #1 & #2" [7.63ms]
(pass) finds "Closes oven-sh#33280, Closes oven-sh#32864 and Closes oven-sh#29696 (the timer in oven-sh#32949 is orthogonal)" [20.29ms]
(pass) finds "Closes oven-sh#33182 and oven-sh#32947 on top of current main (which already has oven-sh#36304 for catalogs)." [16.12ms]
(pass) finds "Fixes #1,\n#2" [7.76ms]
(pass) finds "Fixes #1, #2,\nand #3" [9.27ms]
(pass) finds "Fixes #1\nand #2" [8.57ms]
(pass) finds "Fixes #1\n& #2" [6.80ms]
(pass) finds "Fixes #1 and\n#2" [7.31ms]
(pass) finds "Supersedes oven-sh#39908 (same change, moved from a fork branch)" [13.21ms]
(pass) finds "Supersedes oven-sh#38778 and oven-sh#38391. Carries the entry point arm of oven-sh#35053." [14.43ms]
(pass) finds "Supersedes oven-sh#39193 and keeps its three tests." [11.48ms]
(pass) finds "This supersedes oven-sh#33306 and oven-sh#32803. Their tests are kept here." [13.73ms]
(pass) finds "- This replaces oven-sh#33793. Its 
... (truncated)
Exit: 0
```
</details>
<details><summary>diff hotspot</summary>
```
.github/workflows/close-linked-issues.yml | 950 ++++++++++++++++++++++++++++++
 test/internal/close-linked-issues.test.ts | 598 +++++++++++++++++++
 2 files changed, 1548 insertions(+)
```
</details>
**gate history** · 29 passed · 0 rejected · iteration 9
<details><summary>evidence per changed file</summary>
```
file reads edits tests
.github/workflows/close-linked-issues.yml 6 12 0
test/internal/close-linked-issues.test.ts 3 11 0
```
</details>
<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /