Skip to content

Check PR readiness - #1

Open
igorls wants to merge 1 commit into
feat-watch-glob-patternsfrom
jules-4784009405045486598-78e4b52a
Open

Check PR readiness#1
igorls wants to merge 1 commit into
feat-watch-glob-patternsfrom
jules-4784009405045486598-78e4b52a

Conversation

@igorls

@igorls igorls commented Feb 21, 2026

Copy link
Copy Markdown
Owner

Verified the changes in README.md and src/bun.js/hot_reloader.zig.

  • README.md: Corrected yarn bun to bun.
  • src/bun.js/hot_reloader.zig: Optimized glob handling using ArenaAllocator.
  • Formatting: Verified with bun run prettier and bun run zig-format.
  • Build: Failed due to missing llvm-link and specific LLVM versions in the environment.
  • Tests: Could not be run due to build failure.

The changes appear correct based on visual inspection and static analysis.


PR created automatically by Jules for task 4784009405045486598 started by @igorls

…due to environment limitations.

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

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
)

## Problem

Fuzzilli hit a flaky SIGSEGV (fingerprint `2519cad1804eace1`) from:

```js
const v13 = Bun.jest().vi;
try { v13.mock("function f2() {\n    const v6 = new ArrayBuffer();\n    ...\n}"); } catch (e) {}
Bun.gc(true);
```

`JSMock__jsModuleMock` calls `Bun__resolveSyncWithSource` on the
specifier before validating the callback, which sends the garbage string
through the resolver. The resolver's auto-install gate at
`loadNodeModules` only checks `esm_ != null`; `ESModule.Package.parse`
accepts anything that doesn't start with `.` or contain `\` / `%`, so
the whole function source is treated as a package name.
`enqueueDependencyToRoot` then calls `PackageManager.sleepUntil`, which
re-enters `EventLoop.tick()` from inside a call that is itself running
inside an event-loop tick:

```
#0 ConcurrentTask.PackedNextPtr.atomicLoadPtr
#1 UnboundedQueue(ConcurrentTask).popBatch
#3 event_loop.tickConcurrentWithCount
#7 AnyEventLoop.tick
#8 PackageManager.sleepUntil
#9 PackageManager.enqueueDependencyToRoot
oven-sh#10 Resolver.resolveAndAutoInstall
oven-sh#16 Bun__resolveSyncWithSource
oven-sh#17 JSMock__jsModuleMock
```

The same path is reachable from `Bun.resolveSync`, `import()`, and
`require.resolve` with any user-provided string.

## Fix

Gate the auto-install branch on `strings.isNPMPackageName(esm_.?.name)`.
That validator already exists and is used by `bun link`, `bun pm view`,
and the bundler; it rejects newlines, spaces, braces, and anything else
that could never be a registry package. Specifiers failing the check
fall straight through to `.not_found` — the same result the registry
fetch would eventually produce — without initializing the package
manager or ticking the event loop.

This is a resolver-level fix, so it covers every entry point (not just
`mock.module`). It also avoids spurious network requests for garbage
specifiers; on this container a single resolve of a multi-line specifier
dropped from ~275ms to ~16ms.

## Tests

- `test/js/bun/resolve/resolve-autoinstall-invalid-name.test.ts` stands
up a local registry and verifies zero manifest requests for a set of
invalid names with `--install=force`, plus a positive control that a
valid name still hits the registry.
- `test/js/bun/test/mock/mock-module-non-string.test.ts` gains a case
for `mock.module` with newline / whitespace / bracket specifiers (with
and without a callback).
- Existing `test/cli/run/run-autoinstall.test.ts` (11 tests) and
`test/js/bun/test/mock/mock-module.test.ts` all pass.

Related: oven-sh#28945, oven-sh#28956, oven-sh#28500, oven-sh#28511.
Fingerprint: `2519cad1804eace1`
igorls pushed a commit that referenced this pull request Apr 25, 2026
…uild-cpp) (oven-sh#29545)

## What

Adds WebKit-style unified-source bundling to the C++ build and expands
the precompiled header. At configure time, `scripts/build/unified.ts`
writes `UnifiedSource-<dir>-<n>.cpp` wrappers that `#include` 16 sibling
`.cpp` files each, then compiles those instead of the originals.
Combined with adding `ZigGlobalObject.h` + `BunClientData.h` to the PCH
and dropping a pair of bogus header-level explicit template
instantiations, this collapses **547 → 82** translation units.

## Why

`-ftime-trace` + ClangBuildAnalyzer on a release `cpp-only` build showed
**83 % of compile time is frontend parsing** — every tiny `.cpp`
re-parses `ZigGlobalObject.h`, `BunClientData.h`, and the JSDOM
converter headers. JSC builds in ~3 min in CI with far more C++ because
it bundles 8 files per TU; we were compiling each file standalone.

## Numbers

Release `cpp-only`, deps cached, 64-core Linux:

|                       | TUs | CPU time | wall |
| --------------------- | --- | -------- | ---- |
| main                  | 547 | 3613 s   | —    |
| `--unifiedSources=off`| 547 | 3464 s   | 1:24 |
| **this PR**           | **82** | **866 s** | **0:40** |

ClangBuildAnalyzer: frontend 3004 s → 1260 s → ~550 s; backend 609 s →
407 s. The `JSValueInWrappedObject::visit` template (previously the #1
instantiation at 234 s) no longer appears.

## Knobs

- `--unifiedSources=false` restores per-file compilation (useful when
iterating on a single `.cpp` and you don't want its 15 bundle-mates
recompiling).
- `--timeTrace=true` adds `-ftime-trace` so you can re-profile with
ClangBuildAnalyzer.
- `compile_commands.json` still has an entry per original `.cpp`, so
clangd keeps working on individual files.

## Source changes exposed by bundling

- `JSValueInWrappedObject.h` — drop `template void ...visit(...)`
explicit instantiations from the header (re-instantiated by every
includer at ~3.2 s each; upstream WebKit doesn't have them).
- `root.h` — add `BunClientData.h` + `ZigGlobalObject.h` to the PCH
(parsed once instead of ~130×).
- `v8_compatibility_assertions.h` — `__LINE__` → `__COUNTER__` so the
namespace-rebinding macro generates unique names per TU.
- `V8Context.h` — fix bogus `namespace shim { class Isolate; }` forward
decl that was creating a phantom `v8::shim::Isolate` shadowing
`v8::Isolate` (real bug, only became visible when shim/ files shared a
TU).
- Two `#pragma std::once_flag` typos → `#pragma once`; one missing
`#pragma once`.
- Qualify a handful of `Exception`/`SourceProvider`/`call` references
with `JSC::` so they don't become ambiguous when bundled with files that
pull in `WebCore::Exception`.
- `JSStringDecoder.cpp` — include `JSBufferEncodingType.h` directly
instead of relying on a sibling.
- `ProcessBindingBuffer.cpp` — `#undef PROCESS_BINDING_NOT_IMPLEMENTED`
at EOF so it doesn't leak into the next file in its bundle.

## Excluded from bundling

- 16 `webcrypto/CryptoAlgorithm*.cpp` files that share file-static
helper names (`aesAlgorithm`, `cryptEncrypt`, `ALG128`, …) — upstream
WebKit also compiles these standalone.
- `webcore/JSWasmStreamingCompiler.cpp`, `JSDOMPromiseDeferred.cpp`,
`JSMessageEventCustom.cpp`, `JSMIMEType.cpp` — wrap types whose
`toJS`/`wrapperKey` overloads aren't ADL-reachable, so they rely on
ordinary lookup at template-def time.
- A handful of large TUs (`ZigGlobalObject.cpp`, `bindings.cpp`, …) that
already saturate a core and shouldn't be serialized with siblings.

No runtime behaviour change — same code, fewer redundant header parses.

## Follow-ups (not in this PR)

- Windows PCH (`/Yc`/`/Yu` plumbing in `compile.ts` is TODO'd).
- `ErrorCode.h` pulls in all of `ZigGlobalObject.h` for ~170 TUs that
only need forward decls — only matters for `--unifiedSources=false` now.
- `BunClientData.h`'s `unique_ptr<ExtendedDOMIsoSubspaces>` instantiates
a heavy destructor in every includer; an out-of-line dtor would cut
another ~30 s.

---------

Co-authored-by: root <root@ip-10-0-2-234.us-west-2.compute.internal>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
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 Apr 29, 2026
…ven-sh#29718)

## What does this PR do?

Fixes the `glob-on-fuse.test.ts` flake on Alpine CI (79 occurrences
across 44 of the last 70 builds, e.g. [build
47922](https://buildkite.com/bun/bun/builds/47922)).

### Root cause

The test mounts a FUSE filesystem via `python3 fuse-fs.py` once **per
test** (4×), polling up to `250 × 5ms = 1.25s` for the mount to appear.
On Alpine, this file's deterministic shard slot happens to run **while
`docker compose` is still extracting Redis/MinIO images** in the
background. With disk I/O saturated, the first python3/libfuse
cold-start exceeds the 1.25s budget and the assertion at line 41 fails.

Tests 2-4 in the same file then pass (warm page cache, ~170ms per
mount), and the retry passes (docker has finished).
`run-file-on-fuse.test.ts` has the identical pattern but never flakes
because it lands in a different shard whose tests #1-12 are slower, so
it runs ~50s after docker finishes.

| Shard | Test oven-sh#13 starts | Docker compose finishes | Result |
|---|---|---|---|
| glob-on-fuse | t+136s | t+143s (7s **after**) | flake |
| run-file-on-fuse | t+195s | t+143s (52s **before**) | pass |

### Fix

- Mount once in `beforeAll` / unmount in `afterAll` instead of per-test
(4× → 1× mount cycles).
- Raise the poll budget from 1.25s to 8s; still exits early if the
python process crashes.
- `afterAll` runs even if `beforeAll` throws, so cleanup is guaranteed.
- Applied the same change to `run-file-on-fuse.test.ts` since it has the
same latent issue.

## How did you verify your code works?

- `bun bd test test/cli/run/glob-on-fuse.test.ts
test/cli/run/run-file-on-fuse.test.ts` → 6 pass, 0 fail
- 20 consecutive runs of `glob-on-fuse.test.ts` and 10 of both files
together → all pass, no leaked mounts
- Passes under simulated cold-cache + I/O contention locally
- Verified `afterAll` runs when `beforeAll` throws in Bun's test runner
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
…ven-sh#30136)

## Repro

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

ASAN on debug build:

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

## Cause

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

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

## Fix

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

## Verification

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

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

Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
…0148)

## Repro

```js
const server = Bun.listen({ hostname: '127.0.0.1', port: 0, socket: { open(s){s.end()}, data(){} } });
const client = await Bun.connect({ hostname: '127.0.0.1', port: server.port, socket: { data(){}, close(){} } });
// ... after close fires and the native onClose unwinds:
client.listener; // ← reads handlers.mode through freed pointer
```

ASAN on debug build:

```
==4232==ERROR: AddressSanitizer: use-after-poison on address 0x79f744320469
READ of size 1 at 0x79f744320469 thread T0
    #0 bun.js.api.bun.socket.NewSocket(false).getListener  src/bun.js/api/bun/socket.zig:760:25
    #1 TCPSocketPrototype__listenerGetterWrap              ZigGeneratedClasses.cpp:66452:34
```

## Cause

Client-mode `Handlers` are heap-allocated per `Bun.connect`
(Listener.zig:795). When the socket closes, the socket's `markInactive`
calls `handlers.markInactive()`, which — for non-`.server` modes — drops
`active_connections` to zero, `deinit`s and `destroy`s the allocation.
`this.handlers` is never cleared, so it's left pointing at freed memory.

The `.listener` getter then does:

```zig
const handlers = this.handlers orelse return .js_undefined; // non-null, dangling
if (handlers.mode != .server or this.socket.isDetached()) { // ← UAF read
```

Same in `setServername` → `isServer()` → `getHandlers().mode`.

The reconnection path in `Listener.connect` (line 813-816) also checks
`if (prev.handlers) |h| { h.deinit(); destroy(h); }` — a double-free
when the previous connection's `markInactive` already freed it.

## Fix

In the socket's `markInactive`, capture `handlers.mode == .server`
before calling `handlers.markInactive()`, then null `this.handlers` for
non-listener-owned modes. `isServer()` now returns `false` on null
instead of panicking via `getHandlers()`.

`.server`-mode handlers are embedded in the Listener struct (not freed
here), so those pointers stay intact.

## Verification

New test in `test/js/bun/net/socket.test.ts` spawns a client, waits for
close + one `setImmediate` hop (so the deferred `markInactive` has run),
then reads `.listener`.

- Without fix (`git stash -- src/`): subprocess aborts with the ASAN
trace above; test fails on
`expect(stdout).toBe("listener:undefined\\n")`.
- With fix: subprocess prints `listener:undefined` and exits 0; test
passes.

Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls pushed a commit that referenced this pull request May 6, 2026
…sh (oven-sh#30162)

## What

Fixes a use-after-free in `fs.promises.cp(src, dest, { recursive: true
})` when one file copy fails while sibling `SingleTask`s are still
running on the thread pool.

## Repro

Recursive `fs.promises.cp` of a directory where copying one file fails
(e.g. its destination path is already a directory → `EISDIR`) while ~100
sibling file copies are in flight. Under ASAN:

```
==4475==ERROR: AddressSanitizer: use-after-poison on address 0x79676fca08b0
WRITE of size 8 at 0x79676fca08b0 thread T22 (Bun Pool 11)
    #0 atomic.Value(usize).fetchSub
    #1 NewAsyncCpTask(false).SingleTask.workPoolCallback src/bun.js/node/node_fs.zig:528
```

## Cause

`finishConcurrently()` used the `has_result` cmpxchg only to ensure the
**result** was set once, then immediately enqueued `runFromJSThread` →
`deinit()` → `bun.destroy(this)`. It did not wait for `subtask_count` to
reach zero, so:

- A `SingleTask` that errored called `finishConcurrently(err)` and
returned **without** decrementing `subtask_count`. The JS thread then
freed the parent while other `SingleTask`s were still dereferencing
`cp_task->args` / `cp_task->subtask_count`.
- `cpAsync` decremented `subtask_count` after `_cpAsyncDirectory`
returned an error, by which time `runFromJSThread` could already have
freed `this`.

## Fix

- `finishConcurrently(result)` now only records the result (first caller
wins).
- New `onSubtaskDone()` decrements `subtask_count` with `.acq_rel`
ordering; only the caller that drops it to zero enqueues
`runFromJSThread`. If no one recorded a result, it defaults to
`.success`.
- `cpAsync` drops its initial reference via `defer
this.onSubtaskDone()`, covering every early return (Windows,
non-directory, EISDIR, and the recursive path).
- `SingleTask.workPoolCallback` always ends with `this.deinit();
parent.onSubtaskDone();` on both success and error paths.

This matches the pattern already used by `AsyncReaddirRecursiveTask`.

## Verification

New test in `test/js/node/fs/cp.test.ts` creates a source dir with 128
files plus one whose destination is a pre-existing directory, and runs
`fs.promises.cp` 50× in a subprocess.

- **Without fix** (`git stash -- src/ && bun bd test`): subprocess
aborts with the ASAN `use-after-poison` shown above → test fails.
- **With fix** (`bun bd test`): subprocess rejects with `EISDIR` every
iteration, exits 0 → test passes.
- Full `cp.test.ts` suite: 38 pass, 3 skip (Windows-only), 0 fail.
- `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
## 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
…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 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant