Skip to content

Navigation Menu

Sign in
Sign up

deps: update sqlite to 3.53.400 - #5

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

deps: update sqlite to 3.53.400 #5
github-actions[bot] wants to merge 1 commit into
main from
deps/update-sqlite

Conversation

@github-actions

@github-actions github-actions Bot commented Mar 8, 2026
edited
Loading

Copy link
Copy Markdown

What does this PR do?

Updates SQLite to version 3.53.400

Compare: https://sqlite.org/src/vdiff?from=3.51.2&to=3.53.400

Auto-updated by this workflow

@github-actions github-actions Bot changed the title (削除) deps: update sqlite to 3.52.0 (削除ここまで) (追記) deps: update sqlite to 3.51.300 (追記ここまで) Mar 15, 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>
@github-actions github-actions Bot changed the title (削除) deps: update sqlite to 3.51.300 (削除ここまで) (追記) deps: update sqlite to 3.53.0 (追記ここまで) Apr 12, 2026
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
...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
...before write (oven-sh#30155)
## Repro
```js
Bun.serve({
 port: 0,
 fetch: () =>
 new Response("hello", {
 headers: [
 ["Transfer-Encoding", "gzip"],
 ["Transfer-Encoding", "chunked"],
 ],
 }),
});
// HEAD / → ASAN heap-use-after-free in uWS::HttpResponse::writeHeader
```
The duplicate entries make `FetchHeaders` combine them via
`makeString()`, producing a `StringImpl` held only by the header map —
the minimal condition for the free to actually happen.
StringImpl is allocated via bmalloc which ASAN doesn't instrument by
default; with `Malloc=1` (bmalloc → system heap) the debug build
reports:
```
AddressSanitizer: heap-use-after-free
READ of size 13
 #2 uWS::HttpResponse<false>::writeHeader
 #5 doRenderHeadResponse RequestContext.zig:1378
freed by:
 oven-sh#23 HTTPHeaderMap::remove
 oven-sh#28 doWriteHeaders RequestContext.zig:2303
 oven-sh#29 renderMetadata RequestContext.zig:2209
 oven-sh#30 doRenderHeadResponse RequestContext.zig:1377
```
## Cause
`doRenderHeadResponse()` calls `headers.fastGet(.TransferEncoding)`,
which returns a `ZigString` that **borrows** the header map entry's
`StringImpl` bytes (no ref taken). For an ASCII value, `toSlice()` also
borrows rather than copying. It then calls `this.renderMetadata()`,
whose `doWriteHeaders()` does `headers.fastRemove(.TransferEncoding)`
(and `renderMetadata` also `swapInitHeaders()` + `deref()`s the whole
`FetchHeaders`). When the map held the only reference to the
`StringImpl`, it's destroyed right there — and the very next line
`resp.writeHeader("transfer-encoding", transfer_encoding_str.slice())`
writes the freed bytes to the socket.
The adjacent `Content-Length` branch has the same bug:
`std.fmt.parseInt()` runs on the borrowed slice *after*
`renderMetadata()` has already `fastRemove(.ContentLength)`'d it.
## Fix
- **Transfer-Encoding**: use `toSliceClone()` instead of `toSlice()` so
the value is owned and survives `renderMetadata()`.
- **Content-Length**: parse the integer *before* `renderMetadata()` (and
drop the slice immediately), so the borrowed bytes are never touched
after the header entry is removed. No extra allocation needed since only
the parsed `usize` is used afterwards.
## Verification
New test in `test/js/bun/http/bun-server.test.ts` (inside the existing
`HEAD requests oven-sh#15355` block) spawns a subprocess with `Malloc=1`
(non-Windows), serves HEAD responses whose Transfer-Encoding /
Content-Length values are `makeString()`-combined (sole-owner
StringImpl), and asserts the raw wire output.
```
git stash push -- src/ → test fails with "AddressSanitizer: heap-use-after-free" in stderr
git stash pop → test passes
```
All other tests in the `HEAD requests oven-sh#15355` describe block continue to
pass.
Co-authored-by: robobun <robobun@users.noreply.github.com>
@github-actions github-actions Bot changed the title (削除) deps: update sqlite to 3.53.0 (削除ここまで) (追記) deps: update sqlite to 3.53.100 (追記ここまで) May 10, 2026
igorls pushed a commit that referenced this pull request May 31, 2026
...er (oven-sh#31333)
### Problem
Fuzzing found a second transpiler stack overflow
(`sig:SIGSEGV:nostack`): ~600 nested `{` blocks crash the process.
```js
new Bun.Transpiler({ loader: "tsx", target: "bun", minifyWhitespace: true, deadCodeElimination: true })
 .transformSync("{".repeat(600) + 'class Test1 { static "prop1" = 0; }' + "}".repeat(600));
```
oven-sh#31242 guarded the **expression** recursion (`visit_expr_in_out`,
`print_expr`, DCE helpers), but the **statement** recursion was left
unguarded. Nested blocks stay under `MAX_STMT_DEPTH` (1000) in
`parse_stmt`, then the visit pass recurses through `visit_stmts →
visit_and_append_stmt → s_block → visit_stmts` with no stack check —
each level stacks several multi-KB frames, so a few hundred levels
exhaust the thread's stack (reproduces at depth 800 on a debug build's 8
MB main stack; smaller stacks crash at 600):
```
#5 visit_stmts src/js_parser/visit/mod.rs:1280
#6 s_block src/js_parser/visit/visit_stmt.rs:1627
#7 visit_and_append_stmt src/js_parser/visit/visit_stmt.rs:108
#8 visit_stmts src/js_parser/visit/mod.rs:1336
... (repeats until SIGSEGV)
```
### Fix
Guard the statement recursion the same way the expression recursion
already is:
- `visit_and_append_stmt` now checks `stack_check.is_safe_to_recurse()`
(plus the `reported_stack_overflow` fast-path) and reports "Maximum call
stack size exceeded" instead of descending, mirroring
`visit_expr_in_out`.
- `print_stmt` and `print_if` (which self-recurses for `else if` chains
without passing through `print_stmt`) get the same guard
`print_expr`/`print_binding` already have, so a deep AST printed on a
thread with less stack headroom errors instead of overflowing.
- Removed the `MAX_STMT_DEPTH`/`parse_stmt_depth` hard cap from
`parse_stmt` (review feedback): recursion depth in every phase is now
governed by `StackCheck` alone, matching the Zig parser.
- Guarded `hoist_symbols` the same way: it walks the scope tree before
the visit pass at the full depth the parser allowed, and was only kept
safe previously by the now-removed cap (the 15k-deep
`lots-of-for-loop.js` fixture overflowed it in release builds
otherwise).
With this, every arbitrarily-nestable AST recursion (statements,
expressions, bindings) is stack-checked in all three phases (parse,
visit, print); deep inputs throw a catchable `Maximum call stack size
exceeded` error.
### Verification
New test `deeply nested statement blocks error instead of crashing the
process` in `test/bundler/transpiler/transpiler.test.js` transpiles
nested-block and `else if`-chain shapes at depths 600/800/990 (below the
parse-time cap, deep enough to overflow an unguarded visitor) in a
subprocess and asserts it exits cleanly.
- Without the fix: the subprocess dies with SIGSEGV at depth 800+ (debug
build), so the test fails.
- With the fix: `bun bd test test/bundler/transpiler/transpiler.test.js`
→ 147 pass, 0 fail; the repro above now throws `Maximum call stack size
exceeded`.
@github-actions github-actions Bot changed the title (削除) deps: update sqlite to 3.53.100 (削除ここまで) (追記) deps: update sqlite to 3.53.200 (追記ここまで) Jun 7, 2026
@github-actions github-actions Bot changed the title (削除) deps: update sqlite to 3.53.200 (削除ここまで) (追記) deps: update sqlite to 3.53.300 (追記ここまで) Jun 28, 2026
@github-actions github-actions Bot changed the title (削除) deps: update sqlite to 3.53.300 (削除ここまで) (追記) deps: update sqlite to 3.53.400 (追記ここまで) Jul 26, 2026
igorls pushed a commit that referenced this pull request Aug 21, 2026
oven-sh#35255)
`test/js/bun/http/serve-protocols.test.ts` has been going red on main
(build 78445 darwin-x64 hard, 78462 debian-11-aarch64 hard, plus
78300/78419 with retries), always as
```
error: HTTP3StreamReset fetching "https://127.0.0.1:<port>/echo"
✗ Bun.serve over http/3 > POST echo 1000000 bytes [20169.37ms]
```
Reproduced on Linux by looping the file: the h3 subset alone fails about
17 of 100 runs.
## Cause
The ten concurrent h3 tests share one lsquic client engine and one
unconnected UDP socket. `bsd_create_udp_socket()` sets `IP_RECVERR` on
every UDP socket (for `node:dgram`'s error surfacing, oven-sh#28827), including
QUIC's. When a finished test `proc.kill()`s its server, the client
session is still in the engine and keeps scheduling retransmits /
`NEW_CONNECTION_ID` to an unbound port. With `IP_RECVERR` on, the
resulting ICMP port-unreachable is queued on the shared socket and the
next `sendmmsg` returns `-1 ECONNREFUSED`, even though that call is
sending a datagram to a live peer.
`us_quic_packets_out` reports that as a short return, lsquic clears
`ENPUB_CAN_SEND` for the whole engine and only its one-second
`resume_sending_at` failsafe re-enables it. With several dead sessions
generating ICMPs, every failsafe retry fails the same way and the live
1MB upload never advances; the 20s in CI is two idle-timeout rounds
through `retry_or_fail`.
While tracing that I also found an unsigned underflow in lsquic's
`send_batch` requeue loop: when the first unsent spec in a batch
coalesces multiple packets (`pack_off[0] == 0`, `iovlen > 1`), `end =
&batch->packets[off - 1]` indexes with `UINT_MAX` and only the last
packet of the coalesced group is returned to the connection. The earlier
ones are the INIT ACK and the HSK CRYPTO carrying the client Finished,
so the peer can never complete the handshake. This is the same hang
reached from a different direction (real EAGAIN backpressure instead of
stale ICMP).
## Fix
- `IP_RECVERR` is now opt-in via `LIBUS_UDP_LINUX_RECVERR`, set by
`us_create_udp_socket` when a `recv_error_cb` is provided. `node:dgram`
always passes one and keeps the option; QUIC passes `NULL` and no longer
gets it. This matches libuv's `UV_UDP_LINUX_RECVERR` gating that `bsd.c`
already cited.
- `us_quic_packets_out()` retries once on a non-`EAGAIN`/`ENOBUFS` send
failure before reporting a short return, so a stale `sk_err` that does
surface cannot pause the engine. Both the `sendmmsg` and per-packet
paths now go through `US_FAULT_CHECK(US_FAULT_SENDMSG, ...)` so the
short-return path is reachable from tests.
- `patches/lsquic/requeue-unsent-coalesced.patch` rewrites the requeue
loop's bounds as `[off, off+count)` so every packet in an unsent
coalesced datagram is returned to the connection. The same underflow is
present in upstream lsquic master; I will open a PR there separately.
- `serve-protocols.test.ts` now stops each fixture server gracefully on
stdin close (`server.stop(true)`), matching `serve-http3.test.ts`, so
the pooled client session sees `CONNECTION_CLOSE` instead of leaving the
engine retransmitting to unbound ports.
- `test/js/web/fetch/fetch-http3-syscall-fault.test.ts` injects `EAGAIN`
on the coalesced handshake datagram (the `pack_off[0]==0`, `iovlen>1`
spec the lsquic patch fixes), a one-shot `ECONNREFUSED` that the
retry-once branch consumes, and a burst of `EAGAIN` that the `on_drain`
path recovers from.
## Verification
Release build, looped:
| | before | after |
| --- | --- | --- |
| `serve-protocols -t "http/3"` | 17/100 fail | 2/100 fail |
| `serve-protocols` (full) | 7/100 fail | 4/200 fail |
Debug+ASAN: `serve-protocols`, `serve-http3` (46), `fetch-http3-client`
(52), `fetch-http3-adversarial` (29), `fetch-http3-syscall-fault` (3)
and `dgram.test.ts` all pass, 211 tests total.
The residual ~1-2% is a separate pre-existing bug (the client's 36-byte
HSK CRYPTO is buffered but never flushed when `drain_send_body` writes
the whole 1MB body synchronously from `on_stream_open`); I've handed
that off as its own issue. With CI's retry it is well under the flake
threshold.
### Gate note
The fault-injection hook that makes the new test deterministic lives in
`packages/bun-usockets/src/quic.c`, so `git stash -- src/ packages/`
removes it along with the fix and the fault never fires. The lsquic
piece lives in `patches/` and `scripts/`, which the stash does not
touch. That means a single stashed run passes (no fault, no stall) and a
single unstashed run passes (fault fires, fix handles it), and the gate
cannot distinguish them mechanically. The 300-iteration probe above is
the evidence; the fault-injection tests pin the behavior going forward.
<details>
<summary>lsquic debug trace of the stall</summary>
```
engine: packets out returned 0 (out of 1)
[C919...] event: unsent packet oven-sh#15 ACK_FREQUENCY, size 36
[C919...] sendctl: packet oven-sh#15 has been delayed
engine: send_packets_out: sent 0 packets
... <- no "can send again"; nothing for 1s
engine: failsafe activated: resume sending packets again after timeout
engine: packets out returned 0 (out of 10) <- fails again, live conn's oven-sh#207 included
```
and for the underflow, a batch with `pack_off[0]=0`, `iovlen[0]=3`:
```
engine: packets out returned 0 (out of 2)
event: unsent packet #3 ACK PADDING, size 1059
event: unsent packet #4 ACK CRYPTO, size 87
event: unsent packet #5 NEW_CONNECTION_ID, size 54
event: unsent packet #6 STREAM, size 114
sendctl: packet #6 has been delayed
sendctl: packet #5 has been delayed
... <- #3 and #4 never requeued
[WARN] sendctl: send history gap 2 - 5
```
</details>
<!-- robobun:evidence:begin -->
---
**no test proof** · iteration 1 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/web/fetch/fetch-http3-syscall-fault.test.ts
<!-- robobun:evidence:end -->
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
igorls pushed a commit that referenced this pull request Aug 21, 2026
...rier (oven-sh#36337)
`JSNativeStreamSourceAdapter::m_controller` was a
`JSC::Weak<JSReadableStreamDefaultController>`. When the native pull
promise is rejected (socket fault on a fetch body) the adapter is queued
as the `onNativePullRejected` reaction context, which roots the
**adapter** but not the **controller**: the adapter's only edge to it
was the `Weak`. `FetchTasklet` releases both native `Strong<>`s to the
body stream before that microtask drains, so a GC in between can leave
the entire consumer graph (`controller -> stream -> reader -> pipe op ->
destination -> writer -> readyPromise`) white. The subsequent error
cascade then enqueues the pipe's writes-drained shutdown deferral
against a corpse `op`, and `performPipeShutdownAction(AbortDestination)`
dereferences a swept `readyPromise`:
```
ASSERTION FAILED: result JSObject.h(583) JSGlobalObject *JSC::JSObject::realm() const
#5 JSC::JSObject::realm()
#6 JSC::JSPromise::rejectPromise
#7 JSC::JSPromise::reject
#8 Bun::WebStreams::writableStreamDefaultWriterEnsureReadyPromiseRejected
#9 Bun::WebStreams::writableStreamStartErroring
oven-sh#10 Bun::WebStreams::writableStreamAbort
oven-sh#11 WebCore::performPipeShutdownAction (AbortDestination)
oven-sh#12 WebCore::JSStreamPipeToOperation::onWritesFinishedForShutdown
```
On builds without the assert the same path is a silent write into
freed/reused promise memory.
## Fix
Hold `m_controller` as a visited internal field so a queued adapter
roots the controller directly. The edge is cleared on every terminal
path (`nativeSourcePullRejected`, `nativeSourceCallClose`,
`nativeSourceCancel`); `controller->algorithmContext` is cleared by
`readableStreamDefaultControllerClearAlgorithms`, so the abandoned case
is an ordinary intra-heap cycle mark-sweep collects.
`NewSource::this_jsvalue` is only `Strong` during FileReader I/O, where
pinning the consumer graph is the correct behavior anyway.
With the `Weak` gone the adapter no longer needs a destructor, so it is
now a `JSInternalFieldObjectImpl<5>`: the five JSValue members (handle,
pendingView, closer, drainValue, controller) are internal fields visited
by the base class, with typed accessors at call sites. The scalar
members (chunkSize, flag bitfield, text-decode state) stay as plain
members.
## Verification
`native-source-onclose-leak.test.ts` (the partial-read + `releaseLock`
abandonment tests for Blob/fetch/File sources) continues to pass,
confirming the cycle does not pin. `streams.test.js`,
`pipeTo-signal-leak.test.ts`, `compression.test.ts`, `blob.test.ts` all
pass.
The crash itself is 0/1800 standalone; it reproduces ~1/3 only under a
fault-injected tracer replay. `pipeTo-shutdown-gc.test.ts` exercises the
shape (native body source, socket fault mid-stream, fire-and-forget
`pipeTo` under `collectContinuously`, `AbortDestination` shutdown arm)
as a regression surface.
<!-- robobun:evidence:begin -->
---
**no test proof** · iteration 2 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/web/streams/pipeTo-shutdown-gc.test.ts
<!-- robobun:evidence:end -->
igorls pushed a commit that referenced this pull request Aug 21, 2026
...e cache (oven-sh#37034)
### Problem
On the `13 x64-asan` lane, a test that exercises non-ISO Temporal
calendars from a test callback can abort after a fully green run with a
LeakSanitizer report. Seen in build 89504 on oven-sh#37024, whose
`test/js/bun/bun-object/deep-equals-temporal.test.ts` uses
`[u-ca=hebrew]`:
```
Direct leak of 624 byte(s) in 1 object(s) allocated from:
 #1 icu_75::HebrewCalendar::clone() const
 #2 icu_75::Calendar::createInstance(icu_75::TimeZone*, icu_75::Locale const&, UErrorCode&)
 #3 ucal_open_75
 #4 JSC::TemporalCore::buildCalendarTemplate(WTF::AbstractLocker const&, unsigned int)
 #5 JSC::TemporalCore::withCalendar<JSC::TemporalCore::calendarYear(...)::$_0>(...)
```
The CI annotation titles this `direct leak of 624b in {closure#0}
(src/jsc/JSValue.rs:1664:22)` because that is the first in-repo frame
(the test-runner's `JSValue::call`); everything below it is WebKit/ICU.
### Cause
`TemporalCore::withCalendar`
(`vendor/WebKit/.../temporal/core/CalendarICUBridge.cpp`) keeps up to 8
open `UCalendar` templates in a process-lifetime `LazyNeverDestroyed`
`TinyLRUCache`, one per calendar ID (non-ISO arithmetic, plus pure-ISO
`PlainDateTime.prototype.with`, which reaches the same path unguarded);
LRU eviction `ucal_close`s them, so the set is bounded. The
`CalendarCacheEntry` that owns each `UCalendar` is
`WTF_MAKE_TZONE_ALLOCATED` (bmalloc), which LSan does not scan, so the
libc-allocated `UCalendar` (and the ICU `TimeZone` inside it) is
reported as a direct leak even though it is reachable. Whether a given
run aborts depends on whether some stale stack or register value still
points at the ICU object when LSan scans at exit, hence the
intermittence.
This is the calendar twin of the already-suppressed
`TemporalCore::withTimeZone` entry (same cache design, same
TZone-allocated owner).
### Fix
- Add a `leak:TemporalCore::buildCalendarTemplate` suppression to
`test/leaksan.supp`, mirroring the `withTimeZone` entry. The pattern
anchors on the template builder rather than `withCalendar` itself so
that a future real leak inside one of the many op lambdas `withCalendar`
runs would still be reported; every cached-template allocation carries
the builder frame. (`withTimeZone` has no such builder frame, its
`ucal_open` is inline, so that entry keeps its existing pattern.)
- Drop the `test/no-validate-leaksan.txt` escape hatch oven-sh#37024 added for
`deep-equals-temporal.test.ts`, re-enabling leak validation for it; that
file exercises the suppressed path on the asan lane.
### Verification
On a debug ASAN build, running `bun test
test/js/bun/bun-object/deep-equals-temporal.test.ts` under the CI
leak-validation env (`BUN_DESTRUCT_VM_ON_EXIT=1`,
`detect_leaks=1:abort_on_error=1`, repo suppression file):
- with the new entry: clean exit, 5/5 runs
- without it: LSan abort with the calendar-template stacks above, 3/3
runs
A standalone probe exercising 8 non-ISO calendars plus pure-ISO
`PlainDateTime.with` from a timer callback shows the same split (10/10
aborts without, 10/10 clean with; `print_suppressions=1` attributes
exactly the ICU template allocations to the new entry). Top-level module
code cannot reproduce this: its allocation stacks carry
`JSC::JSModuleLoader::evaluateNonVirtual`, which the suppression file
already covers wholesale. An ASAN-gated test pinning the entry was part
of an earlier revision and was dropped per review; the re-enabled
`deep-equals-temporal.test.ts` covers the path in CI instead.
The Expect-wrapper shutdown leak mentioned in the dropped no-validate
comment is a separate issue tracked in oven-sh#32180: that is `bun test`'s own
finalizer-owned memory, while this cache deliberately survives VM
teardown, so oven-sh#32180 would not prevent this report.
<!-- robobun:evidence:begin -->
---
**no test proof** · iteration 1 · docs-only change; test-proof not
applicable
<!-- robobun:evidence:end -->
---------
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
igorls pushed a commit that referenced this pull request Aug 21, 2026
...ad transforms (oven-sh#37139)
### Repro
`CompressionStream('brotli')` with a chunk over 128 KiB runs the codec
step on a WorkPool thread. Tearing down the VM while that step is in
flight frees the native coder under the pool thread:
```js
// ASAN build, BUN_DESTRUCT_VM_ON_EXIT=1 (the CI test runner sets this)
const s = new CompressionStream("brotli");
const w = s.writable.getWriter();
const big = new Uint8Array(6 << 20);
for (let i = 0; i < big.length; i += 3) big[i] = (i * 2654435761) >>> 24;
w.write(big).catch(() => {});
w.close().catch(() => {});
s.readable.getReader().read().catch(() => {});
setTimeout(() => process.exit(0), 15);
```
```
==ERROR: AddressSanitizer: heap-use-after-free ... thread (Bun Pool 0)
 #0 UpdateNodes vendor/brotli/c/enc/backward_references_hq.c:468
 ...
 #4 BrotliEncoderCompressStream vendor/brotli/c/enc/encode.c:1661
 #5 CompressionStreamCoder::transform src/runtime/webcore/CompressionStreamCoder.rs:367
freed by:
 BrotliEncoderDestroyInstance
 CompressionStreamCoder__destroy
 JSCompressionStream.cpp:176 (CFinalizer)
 JSC::Heap::CFinalizerOwner::finalize -> Heap::lastChanceToFinalize
```
The same free-under-the-pool-thread happens on `worker.terminate()` /
`process.exit()` inside a worker while a large write is in flight
(`WebWorker::shutdown` -> `WebWorker__teardownJSCVM` ->
`lastChanceToFinalize`). Other faces of the same report: READ 1 in
`BrotliEstimateBitCostsForLiterals` / `UpdateNodes`, WRITE 4 in
`StoreAndFindMatchesH10`. `DecompressionStream` has the identical
finalizer shape, and zstd/zlib formats share the path.
### Cause
The stream cell's CFinalizer (registered in the constructor) destroys
`m_coder` unconditionally. During normal operation the in-flight task's
`Strong` root keeps the cell from being swept, and the eager
ClearAlgorithms release already defers on `m_asyncCodecInFlight`. But
`Heap::lastChanceToFinalize` at VM teardown runs every finalizer
regardless of roots, so the coder (brotli ring buffer + hasher, zlib
window, zstd ctx) is freed while the pool thread is still inside
`transform`.
### Fix
Reference-count the coder. The JS cell holds one reference, released
where it released before (finalizer, or the eager ClearAlgorithms path;
both already null the cell's pointer first, so
`CompressionStreamCoder__destroy` keeps its signature and call sites).
Each in-flight `CompressionAsyncCtx` takes its own reference when the
async step is scheduled and drops it with the ctx on the JS thread. The
backend is freed when the last reference drops, so teardown releases the
cell's hold but can no longer free the state under the pool thread. On
the teardown paths where the completion never gets delivered, the coder
is abandoned with the dying process instead of freed early, which is the
bounded-leak tradeoff the worker teardown path already takes elsewhere.
Related: oven-sh#36983 fences `WebWorker::shutdown` on outstanding off-thread
jobs, which closes the worker-terminate door from the other side (and is
still needed for it: after this change, the worker repro's surviving
report moves to `EventLoop::enqueue_task_concurrent` via
`WorkTask::on_finish` on the freed worker loop, which is exactly the bug
that PR addresses, now with a `WorkTask` stack). This change covers what
the fence cannot: the main-thread `BUN_DESTRUCT_VM_ON_EXIT=1` exit path,
and the coder's own lifetime independent of teardown ordering.
### Verification
- New test in `test/js/web/streams/compression.test.ts` (ASAN-gated):
fails on the unfixed build with the ASan report above, passes with the
fix.
- Main-thread repro: 5/5 clean runs with the fix (was UAF on every run
before).
- `test/js/web/streams/compression.test.ts` (37),
`test/regression/issue/18413-all-compressions.test.ts`,
`test/regression/issue/23314/zstd-large-decompression.test.ts`, and the
four node webstreams compression compat tests all pass.
<!-- robobun:evidence:begin -->
---
**[review]** gate passed · iteration 0 · 4 files touched
<details><summary>fails on main (without fix)</summary>
```console
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/streams/compression.test.ts
bun test v1.4.0 (38b3183)
test/js/web/streams/compression.test.ts:
(pass) TransformStream.prototype getters reject native transform subclasses (0) [13.39ms]
(pass) TransformStream.prototype getters reject native transform subclasses (1) [2.50ms]
(pass) TransformStream.prototype getters reject native transform subclasses (2) [2.66ms]
(pass) TransformStream.prototype getters reject native transform subclasses (3) [2.27ms]
(pass) CompressionStream and DecompressionStream > brotli > compresses data with brotli [15.13ms]
(pass) CompressionStream and DecompressionStream > brotli > decompresses brotli data [21.20ms]
(pass) CompressionStream and DecompressionStream > brotli > round-trip compression with brotli [51.32ms]
(pass) CompressionStream and DecompressionStream > zstd > compresses data with zstd [9.46ms]
(pass) CompressionStream and DecompressionStream > zstd > decompresses zstd data [18.82ms]
(pass) CompressionStream and DecompressionStream > zstd > round-trip compression with zstd [36.88ms]
(
... (truncated)
release without fix: 1 skipped
bun test v1.4.0-canary.1 (0ac8ea9)
test/js/web/streams/compression.test.ts:
(pass) TransformStream.prototype getters reject native transform subclasses (0) [0.42ms]
(pass) TransformStream.prototype getters reject native transform subclasses (1) [0.07ms]
(pass) TransformStream.prototype getters reject native transform subclasses (2) [0.06ms]
(pass) TransformStream.prototype getters reject native transform subclasses (3) [0.03ms]
(pass) CompressionStream and DecompressionStream > brotli > compresses data with brotli [0.91ms]
(pass) CompressionStream and DecompressionStream > brotli > decompresses brotli data [0.78ms]
(pass) CompressionStream and DecompressionStream > brotli > round-trip compression with brotli [1.80ms]
(pass) CompressionStream and DecompressionStream > zstd > compresses data with zstd [0.58ms]
(pass) CompressionStream and DecompressionStream > zstd > decompresses zstd data [0.43ms]
(pass) CompressionStream and DecompressionStream > zstd > round-trip compression with zstd [1.02ms]
(pass) CompressionStream and DecompressionStream > zstd > decompresses a multi-frame zstd stream [0.28ms]
(pass) CompressionStream and DecompressionStream > zstd > decompr
... (truncated)
```
</details>
<details><summary>passes on PR (with fix)</summary>
```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/streams/compression.test.ts
bun test v1.4.0 (38b3183)
test/js/web/streams/compression.test.ts:
(pass) TransformStream.prototype getters reject native transform subclasses (0) [13.88ms]
(pass) TransformStream.prototype getters reject native transform subclasses (1) [2.67ms]
(pass) TransformStream.prototype getters reject native transform subclasses (2) [2.66ms]
(pass) TransformStream.prototype getters reject native transform subclasses (3) [2.15ms]
(pass) CompressionStream and DecompressionStream > brotli > compresses data with brotli [15.57ms]
(pass) CompressionStream and DecompressionStream > brotli > decompresses brotli data [22.42ms]
(pass) CompressionStream and DecompressionStream > brotli > round-trip compression with brotli [51.98ms]
(pass) CompressionStream and DecompressionStream > zstd > compresses data with zstd [10.10ms]
(pass) CompressionStream and DecompressionStream > zstd > decompresses zstd data [18.91ms]
(pass) CompressionStream and DecompressionStream > zstd > round-trip compression with zstd [38.62ms]
... (truncated)
release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 820ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/12] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 239 extern-C blocks audited
[1/12] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
 nightly-2026年07月20日-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026年07月19日)
�[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m Compiling�[0m bun_brotli
... (truncated)
```
</details>
<details><summary>diff hotspot</summary>
```
.../bindings/webcore/streams/JSCompressionStream.h | 7 ++--
 .../webcore/streams/JSCompressionStreamShared.h | 1 +
 src/runtime/webcore/CompressionStreamCoder.rs | 43 +++++++++++++++----
 test/js/web/streams/compression.test.ts | 49 +++++++++++++++++++++-
 4 files changed, 87 insertions(+), 13 deletions(-)
```
</details>
**gate history** · 2 passed · 0 rejected · iteration 0
<details><summary>evidence per changed file</summary>
```
file reads edits tests
src/jsc/bindings/webcore/streams/JSCompressionStream.h 2 2 0
...sc/bindings/webcore/streams/JSCompressionStreamShared.h 2 2 0
src/runtime/webcore/CompressionStreamCoder.rs 3 6 0
test/js/web/streams/compression.test.ts 1 1 0
```
</details>
<!-- robobun:evidence:end -->
igorls pushed a commit that referenced this pull request Aug 21, 2026
...wo parallel vecs (oven-sh#39145)
### Problem
- `LOLHTMLContext` in `src/runtime/api/html_rewriter.rs` keeps two vecs,
`selectors` and `element_handlers`, that describe one thing: entry `i`
of each is the selector and the handler object from the same
`rewriter.on(selector, handlers)` call.
- The pairing is only held up by convention: `on_()` pushes to both,
`build_settings()` zips them back together, and a doc comment plus an
invariant comment explain it. The mordant `parallel_vecs` lint flags
this (the one baselined finding for this file).
### Fix
- Add `ElementHandlerEntry { selector, handler: Box<ElementHandler> }`
and store `element_handlers: Vec<ElementHandlerEntry>`. One vec, one
push in `on_()`, and `build_settings()` destructures each entry instead
of zipping.
- No behavior change: the same values are pushed in the same order, the
handler is still boxed (the lol-html closures built in
`build_settings()` hold raw pointers into the box, so it must not move
when the vec reallocates), and the body of the `build_settings()` loop
is unchanged. The `#[expect(clippy::vec_box)]` comes off
`element_handlers` because it is no longer a `Vec<Box<_>>`;
`document_handlers` keeps its own.
- Remove the `parallel_vecs:src/runtime/api/html_rewriter.rs` line from
`mordant-baseline.toml`.
- Tests, in `test/js/workerd/html-rewriter.test.js` (`on()
registrations`), pin down the two things this storage has to get right.
They pass before and after this change, since it is a refactor:
- Many selectors registered on one rewriter, with two rejected `on()`
calls in the middle, each still run the handlers they were registered
with, on two transforms of the same rewriter.
- `on()` called from inside a handler, often enough to reallocate the
registry while lol-html is still calling the handlers registered before
the transform started: the running transform is unaffected and the next
one picks the additions up. With the `Box` removed from
`ElementHandlerEntry` this test fails under ASAN with a
heap-use-after-free (report in the details below), so the boxing is now
covered rather than only commented.
- Verified:
- `bun bd test` on `test/js/workerd/html-rewriter.test.js` (165 tests,
including the new ones), `html-rewriter-end-error.test.ts`,
`html-rewriter-leak.test.ts`,
`test/js/web/html/html-rewriter-doctype.test.ts` and the HTMLRewriter
regression tests: all pass.
 - `cargo clippy -p bun_runtime --no-deps`: clean.
- `cargo dylint --all -p bun_runtime` with this baseline: nothing over
the baseline. The same command with the baseline line removed but the
source change stashed reports exactly the one `parallel_vecs` finding
for this file, so the removed line is the one this change fixes.
- Regenerating the baseline with `MORDANT_BASELINE_WRITE=1` also drops
two entries this PR does not touch
(`always_unwrapped_option:src/install/PackageInstall.rs`,
`narrowed_two_ways:src/runtime/node/node_crypto_binding.rs`); those
findings were already fixed on main by other changes and are left for a
separate cleanup.
### Background
- `HTMLRewriter.on(selector, handlers)` parses the CSS selector with
lol-html and wraps the JS handler object in an `ElementHandler` (the
protected `element`/`comments`/`text` callbacks). Nothing is handed to
lol-html at that point; registrations are collected in `LOLHTMLContext`,
which is shared by the rewriter and every transform it starts, because
`transform()` can run more than once.
- `build_settings()` runs at transform time and turns each registration
into a `(selector, ElementContentHandlers)` pair for lol-html. Its
closures capture a `NonNull<ElementHandler>` pointing into the heap
allocation owned by the `Box`, which is why the handler has to stay
boxed even though clippy would normally suggest otherwise. An `on()`
call after a transform has started (for example from inside a handler)
pushes onto the same vec, which is what makes the reallocation case
reachable from JS.
- `mordant-baseline.toml` is the ratchet for the mordant lint pack run
by the Rust lints workflow: it records the accepted number of findings
per (lint, file), and CI reports anything above those counts. Removing
the line here means a reintroduction of the pattern in this file would
be reported.
<details>
<summary>ASAN report from the new test with the Box removed from
ElementHandlerEntry</summary>
```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 8
 #3 <ElementHandler as HandlerLike>::global src/runtime/api/html_rewriter.rs
 #4 handler_callback::<ElementHandler, Element, ...> src/runtime/api/html_rewriter.rs
 #5 ElementHandler::on_element src/runtime/api/html_rewriter.rs
 #6 build_settings::{closure#0} src/runtime/api/html_rewriter.rs
 #8 lol_html ContentHandlersDispatcher::handle_start_tag
freed by thread T0 here:
 oven-sh#13 RawVec<ElementHandlerEntry>::grow_one
 oven-sh#15 Vec<ElementHandlerEntry>::push
 oven-sh#16 HTMLRewriter::on_ src/runtime/api/html_rewriter.rs
```
</details>
<!-- robobun:evidence:begin -->
---
**no test proof** · iteration 2 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/workerd/html-rewriter.test.js
<!-- robobun:evidence:end -->
---------
Co-authored-by: Alistair Smith <hi@alistair.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

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