Skip to content

deps: update elysia to 1.4.29 - #3

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
deps/update-elysia
Open

deps: update elysia to 1.4.29#3
github-actions[bot] wants to merge 1 commit into
mainfrom
deps/update-elysia

Conversation

@github-actions

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

Copy link
Copy Markdown

What does this PR do?

Updates elysia to version 1.4.29

Compare: elysiajs/elysia@1.4.12...1.4.29

Auto-updated by this workflow

@github-actions
github-actions Bot force-pushed the deps/update-elysia branch from ce6f10a to a498903 Compare March 1, 2026 04:44
@github-actions github-actions Bot changed the title deps: update elysia to 1.4.25 deps: update elysia to 1.4.26 Mar 1, 2026
@github-actions
github-actions Bot force-pushed the deps/update-elysia branch from a498903 to 86d897c Compare March 8, 2026 04:37
@github-actions github-actions Bot changed the title deps: update elysia to 1.4.26 deps: update elysia to 1.4.27 Mar 8, 2026
@github-actions
github-actions Bot force-pushed the deps/update-elysia branch from 86d897c to 783cbaa Compare March 22, 2026 04:42
@github-actions github-actions Bot changed the title deps: update elysia to 1.4.27 deps: update elysia to 1.4.28 Mar 22, 2026
igorls pushed a commit that referenced this pull request Apr 9, 2026
Fixes a segfault when reading `.fd` on the result of `Bun.listen({ tls:
{ ... } })`.

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

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

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

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

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

Found by Fuzzilli.

---------

Co-authored-by: robobun <robobun@users.noreply.github.com>
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
…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
…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
…ion getters (oven-sh#30078)

## Problem

`server.upgrade(req, opts)` reads `Sec-WebSocket-Key` /
`Sec-WebSocket-Protocol` / `Sec-WebSocket-Extensions` from
`request.getFetchHeaders()` via `FetchHeaders.fastGet`, which returns a
`ZigString` that **borrows** directly from the header map entry's
`StringImpl` (`bindings.cpp` `WebCore__FetchHeaders__fastGet_` →
`Zig::toZigString(StringView)`, no ref taken).

It then invokes the `opts.data` / `opts.headers` getters — arbitrary
user JS — and only afterwards passes those borrowed slices to
`resp.upgrade()`.

A getter that mutates `req.headers` (e.g.
`req.headers.set('sec-websocket-key', ...)`) drops the sole ref on the
original `StringImpl` (`HTTPHeaderMap::set` does a `RefPtr` assignment),
freeing it. `resp.upgrade()` then reads freed memory for the
key/protocol/extensions.

```js
Bun.serve({
  fetch(req, server) {
    req.headers; // materialize FetchHeaders
    server.upgrade(req, {
      get data() {
        req.headers.set('sec-websocket-key', 'x'); // frees the borrowed StringImpl
        return undefined;
      },
    });
  },
  websocket: { message() {} },
});
```

The re-entrancy guard after the getters only checks `isAbortedOrEnded()
/ didUpgradeWebSocket()`, not header mutation. The `opts.headers` path
was already defensively cloning with `toSliceClone` (because
`fastRemove` there frees the backing); the `request.headers` path was
missed.

## Fix

Clone `sec_websocket_key` / `protocol` / `extensions` into owned
`ZigString.Slice` storage immediately after reading them from
`request.getFetchHeaders()`, so the bytes stay valid across the option
getters and `resp.upgrade()`. The `opts.headers` override path reuses
the same owned slots (freeing the previous clone first).

## Verification

New test in
`test/js/bun/websocket/websocket-server-upgrade-reentrant.test.ts`
spawns a subprocess with `Malloc=1` (routes bmalloc → system heap so
ASAN observes `StringImpl` frees) and has an `opts.data` getter
overwrite all three `Sec-WebSocket-*` headers.

**Before** (src/ stashed, `bun bd test`):
```
==ERROR: AddressSanitizer: heap-use-after-free
  #3 uWS::HttpResponse<false>::upgrade ... HttpResponse.h:269
  #6 server.zig:1076 (resp.upgrade call)
(fail) server.upgrade() clones Sec-WebSocket-* from request.headers before running option getters
```

**After**: all three tests in the file pass.

Also fails on `USE_SYSTEM_BUN=1` (release, no ASAN) — with `Malloc=1`
the system allocator reuses the freed slot and the WebSocket client
rejects the handshake (bad `Sec-WebSocket-Accept` / mismatched
protocol).

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
…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
…worker panic, never retry (oven-sh#30216)

## What

`bun test --isolate` / `--parallel` crashes when a test file loads a
native addon whose deferred napi finalizers outlive the file. The
`--parallel` coordinator then silently retries the file once, which
masks the panic and lets the run exit 0.

Fixes oven-sh#30205, oven-sh#30191. Supersedes oven-sh#30214 (same NapiEnv fix, but without
the coordinator change, the `cleanup_hooks` retarget, or a test that
actually reproduces on unpatched `main`).

## Reproduction

```sh
git clone https://github.com/workglow-dev/libs && cd libs
bun i && bun run build:packages
bun test --timeout=30000 --parallel=4 packages/test/src/test/{util,task}/*.test.ts
```

On `main` (d484fd6), 3–4 workers crash per run with either
```
ASSERTION FAILED: isMarked(cell)
  JavaScriptCore/heap/Heap.cpp:1232 : void JSC::Heap::addToRememberedSet(const JSCell *)
```
or (when the slot is already being reallocated)
```
ASSERTION FAILED: m_cellState == CellState::DefinitelyWhite
  JavaScriptCore/JSCellInlines.h:69 : JSC::JSCell::JSCell(VM &, Structure *)
```
and in release builds the segfaults at `0x68` / `0xD0` reported in
oven-sh#30205.

## Root cause

Frame-pointer walk from the assertion:

```
#3  Bun::NapiHandleScope::open(Zig::GlobalObject*, bool)
#4  NapiHandleScope__open
#6  napi.Finalizer.run
#7  napi.NapiFinalizerTask.runOnJSThread
oven-sh#10 event_loop.tick
oven-sh#11 event_loop.waitForPromise
oven-sh#13 VirtualMachine.loadEntryPointForTestRunner   ← next test file
```

`NapiEnv::m_globalObject` is a raw `Zig::GlobalObject*`. For
non-experimental addons (`nm_version != NAPI_VERSION_EXPERIMENTAL`,
which is ~every real-world addon — sharp, better-sqlite3, etc.),
`napi_wrap`/`napi_create_external` finalizers are **deferred** to the
event loop as `NapiFinalizerTask` rather than run inside GC sweep.

Objects rooted on the old global (module graph, `globalThis.*`) only
become collectable when `Zig__GlobalObject__createForTestIsolation` runs
`gcUnprotect(oldGlobal)`. The `DeferGC` from oven-sh#29573 ends at that
function's `}`, so the next GC runs there, collects those objects, and
enqueues their finalizers. Those tasks then run on the very next
`eventLoop().tick()` — inside `loadEntryPointForTestRunner`'s
`waitForPromise` for file N+1. `Finalizer.run` opens a `NapiHandleScope`
via `env->globalObject()`, which reads `NapiHandleScopeImplStructure()`
off the dead cell and writes `m_currentNapiHandleScopeImpl` on it →
write barrier on an unmarked cell.

The `--parallel` coordinator's `reapWorker` then re-queued the file once
(`retries[idx] < 1`) into a fresh worker with no stale `NapiEnv`, which
passed — so the run reported 0 fail despite multiple Bun panics in the
log.

## Fix

**NapiEnv retarget** (`ZigGlobalObject.cpp`, `napi.h`):
`Zig__GlobalObject__createForTestIsolation` now calls
`newGlobal->adoptNapiEnvsForTestIsolation(oldGlobal)` before
`gcUnprotect`. Each `NapiEnv::m_globalObject` is repointed at the new
global and the `Ref<NapiEnv>`s are moved over, so late finalizers open
handle scopes on a live global and the envs stay owned after the old
global is swept. `VirtualMachine.swapGlobalForTestIsolation` also
repoints `rare_data.cleanup_hooks[*].globalThis` so `CleanupHook.eql()`
stays accurate.

**No retry, abort on panic** (`Coordinator.zig`): removed the per-file
retry. A worker that dies mid-file is counted as one failure. If it died
by a fatal signal (SIGILL/SIGTRAP/SIGABRT/SIGBUS/SIGFPE/SIGSEGV/SIGSYS —
Bun's own `@trap()`, a JSC/WTF assertion, or native-addon crash), the
whole run aborts with `error: a test worker process crashed with <SIG>
while running <file>`. `process.exit()` / SIGKILL are still just a
per-file failure and the run continues.

## Verification

- `test/regression/issue/30205.test.ts` — 4 tests. Adds a tiny
non-experimental addon (`isolate_finalizer_addon.c`) and a fixture
pattern (`Bun.gc(true)` + module-scope `await 0` + objects rooted on
`globalThis`) that crashes **8/8** on unpatched `main` and passes 8/8
with this change.
- `workglow-dev/libs` full 201-file unit suite: 3× clean `--parallel=4`
runs (was 3–4 crashes/run).
- Gate: `git stash -- src/ && bun bd test
test/regression/issue/30205.test.ts` → 3/4 fail; with fix → 4/4 pass.
- `test/cli/test/isolation.test.ts`,
`test/regression/issue/29519.test.ts` → pass (one pre-existing unrelated
timeout in isolation.test.ts, same as oven-sh#29573).
- `test/cli/test/parallel.test.ts` → all tests I touched pass; the 3
timing-sensitive scale-up/work-steal tests that fail in this container
fail identically on unmodified `main`.

---------

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 31, 2026
…ven-sh#31219)

### What does this PR do?

Fixes a fuzzer-reported SIGSEGV (fingerprint `a8d2694be898a53f`) in the
`Bun.jest()` / `expect` statics area. The reported reproducer exercises
`Bun.jest().expect`, `expect.extend()`, and `new` on an expect static:

```js
const v2 = Bun.jest().expect;
try { v2.extend(); } catch (e) {}
const t6 = v2.arrayContaining;
new t6();
Bun.gc(true);
```

**Root cause (primary fix):** matchers registered through
`expect.extend()` are wrapped in `JSWrappingFunction`.
`JSWrappingFunction::create` passed `nullptr` as the native constructor
to `VM::getHostFunction`. JSC only treats
`callHostFunctionAsConstructor` as "not constructible", so the wrapper
was considered constructible with a null native constructor — `new
expect.someCustomMatcher()` jumps straight to address 0:

```
Thread 1 received signal SIGSEGV, Segmentation fault.
#0  0x0000000000000000 in ?? ()
#3  llint_op_construct ()
```

Deterministic repro (crashes on current main, raw SIGSEGV with no output
— matching the fuzzer's crash signature):

```js
const e = Bun.jest().expect;
e.extend({ myMatcher() { return { pass: true, message: () => "" }; } });
new e.myMatcher();
```

The fix passes `callHostFunctionAsConstructor` so `new` on a wrapped
matcher throws `TypeError: function is not a constructor` like other
native functions.

**Secondary hardening (same area):** if
`Bun__Jest__createTestModuleObject` ever fails it returns an empty
`JSValue`, and the `m_lazyTestModuleObject` initializer called
`toObject()` on it — a null-cell dereference. The initializer now falls
back to a plain object, `Bun__Jest__testModuleObject` surfaces the
pending exception, `Bun.jest()` maps it to a thrown JS error, and the
`xdescribe` arm of `create_test_module` propagates its error instead of
returning an empty value as success.

### How did you verify your code works?

- `new (expect.extend-registered matcher)()` segfaults on the baked
build and throws a `TypeError` with this change.
- Regression test added to `test/js/bun/test/bun-test.test.ts`: it
spawns a subprocess that registers a custom matcher, constructs it (plus
the original fuzzer shape: `extend()` with no args, `new
expect.arrayContaining`), runs `Bun.gc(true)`, and asserts a clean exit.
The test fails on the baked build (`USE_SYSTEM_BUN=1`) with the
subprocess dying from SIGSEGV, and passes with this change.
- `bun bd test` on `bun-test.test.ts`, `expect-extend.test.js`,
`jest-extended.test.js`, `expect-extend-asymmetric-match-throw.test.ts`,
`expect-extend-preload.test.ts`, `describe.test.ts`,
`jest-each.test.ts`, `expect-symbol-toPrimitive-crash.test.ts` — all
pass.
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 force-pushed the deps/update-elysia branch from 783cbaa to 094fc3a Compare June 21, 2026 05:44
@github-actions github-actions Bot changed the title deps: update elysia to 1.4.28 deps: update elysia to 1.4.29 Jun 21, 2026
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