Skip to content

Navigation Menu

Sign in
Sign up

deps: update lolhtml to v2.2.0 - #4

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

deps: update lolhtml to v2.2.0 #4
github-actions[bot] wants to merge 1 commit into
main from
deps/update-lolhtml-2

Conversation

@github-actions

@github-actions github-actions Bot commented Jan 26, 2025

Copy link
Copy Markdown

What does this PR do?

Updates lolhtml to version v2.2.0

Auto-updated by this workflow

sroussey pushed a commit that referenced this pull request Jul 17, 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>
sroussey pushed a commit that referenced this pull request Jul 17, 2026
×ばつ 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 #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>" data-pjax="true" href="/index.cgi/contrast/https://github.com/sroussey/bun/commit/f8fee8d825c6e85843915f904579bf669114e41b">test --isolate: retarget NapiEnv at new global; --parallel: abort on ...
...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
#10 event_loop.tick
#11 event_loop.waitForPromise
#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: ×ばつ 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>
sroussey pushed a commit that referenced this pull request Jul 17, 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.
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...letes mid-read (oven-sh#31959) [publish images]
Fixes a use-after-free in the HTTP client's proxy tunnel close path
(Sentry BUN-2VY8, ~10 events/day on Windows release builds; reproduces
deterministically under ASAN on all platforms).
## Repro
`fetch()` through an HTTP CONNECT proxy to an HTTPS origin, where the
origin's final response bytes and its TLS `close_notify` reach the
client in a single TCP batch (origin writes the response and immediately
closes). The regression test builds exactly that: a local CONNECT proxy
that holds origin-to-client bytes after the handshake and flushes
session tickets + response + close_notify in one write.
On an unfixed ASAN build:
```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 8 thread T11 (HTTP Client)
 #0 Option<RefPtr<ProxyTunnel>>::as_ref
 #1 bun_http::proxy_tunnel::on_close src/http/ProxyTunnel.rs:525
 #2 SSLWrapper<*mut HTTPClient>::trigger_close_callback src/uws/lib.rs:802
 #3 SSLWrapper<*mut HTTPClient>::handle_reading src/uws/lib.rs:1022
 #4 SSLWrapper<*mut HTTPClient>::handle_traffic
 #5 SSLWrapper<*mut HTTPClient>::receive_data
 #6 ProxyTunnel::receive src/http/ProxyTunnel.rs:751
freed by:
 AsyncHTTP::on_async_http_callback_raw src/http/AsyncHTTP.rs:813
 HTTPClient::send_progress_update_without_stage_check src/http/lib.rs:3793
```
## Cause
1. `handle_reading` processes the batch: `SSL_read` returns the body
bytes, the next `SSL_read` hits `close_notify`
(`SSL_ERROR_ZERO_RETURN`), which sets `received_ssl_shutdown` and
`sent_ssl_shutdown` before flushing the already-decrypted bytes through
the data callback.
2. The data callback completes the response. The done path runs
`close_proxy_tunnel(true)` -> `ProxyTunnel::shutdown()` ->
`SSLWrapper::shutdown(true)`, which hits the already-shut-down early
return (`sent_ssl_shutdown || fatal_error`) and returns **without
setting `closed_notified`**. The result callback then frees the
`ThreadlocalAsyncHTTP` embedding the `HTTPClient`, the exact pointer
stored in the wrapper's `handlers.ctx`.
3. Control returns to `handle_reading`. Its liveness guard
(`ssl.is_none() || closed_notified()`) passes because neither is set, so
`trigger_close_callback()` invokes `on_close(handlers.ctx)` on the freed
client. When the allocation has been recycled, `on_close` can ref or
close a different request's tunnel instead of faulting.
## Fix
`src/uws/lib.rs`: when `SSLWrapper::shutdown(fast_shutdown=true)` takes
the already-shut-down early return, fire `trigger_close_callback()`
(idempotent via `closed_notified`) so the wrapper is marked closed
before the owner detaches and frees `handlers.ctx`. A fast shutdown is a
full teardown, and the normal fast-shutdown path already fires the close
callback unconditionally; this only closes the gap where the SSL-level
shutdown had already happened.
Graceful `shutdown(false)` (node:tls half-close via UpgradedDuplex /
WindowsNamedPipe) is unchanged, so reads after a sent `close_notify`
keep working.
## Verification
New test in `test/js/bun/http/proxy.test.ts` (`test.skipIf(!isASAN)`,
the UAF is only deterministic under ASAN): fails on an unfixed ASAN
debug build with the heap-use-after-free above, passes with the fix.
Full `proxy.test.ts` (46 tests) plus `node-tls-connect`,
`node-tls-upgrade`, `node-tls-duplex-close-throw-uaf`,
`node-tls-socket-allow-half-open-option`, `node-tls-server`,
`fetch-tls-cert`, and `node-https-checkServerIdentity` suites pass.
## Note on the asan-lane CI failure (oven-sh#32144)
The intermittent LeakSanitizer failure on the x64-asan shard (deferred
napi finalizers parked on a never-drained cleanup-hook list at `bun
test` exit) is being fixed in oven-sh#32146, which carries the same
`global_exit()` drain plus a hooks-only guard that skips pending
`napi_wrap` finalizers on undrained-loop exits. A subset version of that
fix was briefly on this branch (e59bc1d) but without the hooks-only
guard it made `test/js/third_party/duckdb/duckdb-basic-usage.test.ts`
SEGV at exit on the asan lane (build 62135), exactly the failure mode
oven-sh#32146's guard prevents, so it was reverted (61f9e70). This PR is
scoped to the proxy-tunnel UAF; its asan lane can still intermittently
hit the pre-existing oven-sh#32144 leak until oven-sh#32146 lands.
## Related PRs
- oven-sh#30606 addresses the same crash signature but patches only the `.zig`
reference files, which are no longer compiled; this PR fixes the
shipping Rust implementation.
- oven-sh#31952 fixes the same UAF by calling a new `mark_close_notified()`
helper from `ProxyTunnel::shutdown` (silently setting the flag at one
call site, with `close_raw` exempted). This PR instead closes the gap
inside `SSLWrapper::shutdown(true)` itself, so every fast-shutdown
caller (`ProxyTunnel::shutdown`, `ProxyTunnel::close_raw`,
`UpgradedDuplex::close`, `WebSocketProxyTunnel::shutdown`) gets the same
"no callbacks after teardown" guarantee without new wrapper API or a
shutdown/close_raw asymmetry. The close callback is fired rather than
suppressed, so the error teardown path keeps delivering `on_close` ->
`close_and_fail` exactly once (idempotent via `closed_notified`). Test
here is a deterministic single-shot repro (the test proxy reassembles
TLS records and flushes tickets + response + close_notify in one write)
rather than an iteration loop.
---------
Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...e re-enters the event loop (oven-sh#32597)
Sentry BUN-2WJA / BUN-2WKB (~290 events combined, Windows x86_64,
`http_server=True`, bun 1.2.23 through 1.3.14):
```
Segmentation fault at address 0xFFFFFFFFFFFFFFFF
 endWithSink src/runtime/webcore/Sink.zig:577
 endFromJS src/runtime/webcore/streams.zig:1200
 finalize src/runtime/webcore/streams.zig:1301
 clearAndFree src/collections/baby_list.zig:148
 memset (fault at 0xFFFFFFFFFFFFFFFF)
```
## Cause
The generated `JSReadable*Controller` `end()` and `close()` host
functions (`src/codegen/generate-jssink.ts`) stash `m_sinkPtr` in a
local, call `controller->detach()`, and only afterward dereference the
stashed pointer via `endWithSink()` / `${name}__close()`:
```cpp
void *ptr = controller->wrapped();
controller->detach(); // runs onClose JS synchronously
return ${name}__endWithSink(ptr, lexicalGlobalObject); // derefs ptr
```
`detach()` invokes the stored `onClose` callback. For a `type: "direct"`
stream this is `readDirectStream`'s `close(stream, reason)`, which calls
`underlyingSource.cancel()`. That is arbitrary user code running while
`ptr` is still live on the C++ stack.
If the stream's `pull()` promise has already settled,
`RequestContext::on_resolve_stream` is sitting in the microtask queue.
Any path from `cancel()` that drains microtasks (e.g. the server-side
drain points in `on_response` / `do_render_with_body`, or an explicit
`drainMicrotasks()`) runs `handle_resolve_stream`, which calls
`destroy_sink` and frees the `HTTPServerWritable`. `endWithSink(ptr)`
then enters `end_from_js` on the freed allocation; `finalize()` reads
garbage for `pooled_buffer` / `buffer.cap` / `buffer.ptr` and faults in
the `memset` the allocator's free-scrub path performs.
The same ordering appears in the Rust port (`streams.rs` / `Sink.rs`)
unchanged.
## Fix
In `${controller}__end` and `${controller}__close`, finish the native
sink operation before any JS runs:
1. Call `${name}__controllerDetached(ptr, controller)` and null
`m_sinkPtr` up front (so `end_from_js`'s own `signal.close()` stays a
no-op, matching the previous behaviour, and so the later `detach()`
won't touch the native side again).
2. Run `endWithSink(ptr)` / `close(ptr)`.
3. Call `controller->detach()` last. With `m_sinkPtr` already null it
only clears `m_onPull` and fires `onClose`; by now we hold no reference
into the sink, so re-entrant teardown is safe.
## Verification
New ASAN-gated test in
`test/js/bun/http/serve-direct-readable-stream.test.ts` reproduces the
exact UAF deterministically by draining microtasks from the stream's
`cancel()` callback (the test uses
`require("bun:jsc").drainMicrotasks()` to force the drain that the
production crash hits via the server's own drain points).
<details>
<summary>ASAN output on the unfixed build</summary>
```
==22203==ERROR: AddressSanitizer: heap-use-after-free on address 0x6ee5f87602ca
READ of size 1 at 0x6ee5f87602ca thread T0
 #0 HTTPServerWritable::end_from_js src/runtime/webcore/streams.rs:1831
 #2 JSSink::js_end_with_sink src/runtime/webcore/Sink.rs:1107
 #4 WebCore::JSReadableHTTPResponseSinkController__end JSSink.cpp:620
freed by thread T0 here:
 #10 HTTPServerWritable::destroy src/runtime/webcore/streams.rs:1950
 #11 RequestContext::destroy_sink src/runtime/server/RequestContext.rs:1930
 #12 RequestContext::handle_resolve_stream src/runtime/server/RequestContext.rs:2680
 #13 RequestContext::on_resolve_stream src/runtime/server/RequestContext.rs:2716
 ...
 #24 JSC::VM::drainMicrotasks()
```
</details>
With the fix the fixture completes normally. Existing suites
(`serve.test.ts`, `bun-server.test.ts`,
`direct-readable-stream.test.tsx`, `streams.test.js`, the sink leak
tests) show no new failures against the unfixed build.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...sweep (oven-sh#32729)
### Crash
```
ASSERTION FAILED: vm().currentThreadIsHoldingAPILock() => vm().heap.mutatorState() != MutatorState::Sweeping
vendor/WebKit/Source/JavaScriptCore/runtime/JSCell.cpp(179) : bool JSC::JSCell::validateIsNotSweeping() const
```
Backtrace (from a release-asan build with asserts):
```
#3 JSC::JSCell::validateIsNotSweeping()
#4 JSC::JSCell::classInfo() const
#5 WTF::uncheckedDowncast<WebCore::JSResumableFetchSink>(JSValue const&)
#6 ResumableFetchSinkPrototype__ondrainSetCachedValue
#7 bun_runtime::webcore::fetch::fetch_tasklet::FetchTasklet::ignore_remaining_response_body
#8 JSC::WeakBlock::sweep() <- inside GC sweep (Weak finalizer)
#9 JSC::WeakSet::sweep()
#10 JSC::PreciseAllocation::sweep()
#12 JSC::Heap::finalize()
#21 JSC::LocalAllocator::allocateSlowCase
#23 JSC::ErrorInstance::create <- ordinary allocation kicked off GC
```
Found by the syscall fault-injection fuzzer's client-side grammar
scenario (fetch/node:http with abort + transient errno on the client
socket). Reproduces ~4/5 under `BUN_JSC_collectContinuously=1`.
### Cause
`FetchTasklet::on_response_finalize` is the
`WeakRefOwner<FetchResponse>::finalize` callback and runs inside
`WeakBlock::sweep` while `MutatorState == Sweeping`. When the response
body is `Locked` without a pending promise or stream it calls
`ignore_remaining_response_body()`, which called:
- `ResumableSink::detach_js()`: writes the sink wrapper's cached
`ondrain` / `oncancel` / `stream` slots via the generated
`ResumableFetchSinkPrototype__*SetCachedValue` helpers. Each does
`uncheckedDowncast<JSResumableFetchSink>(thisValue)`, which reaches
`JSCell::classInfo()` and then issues a write barrier on the wrapper
cell.
- `clear_stream_handlers()`: reaches `ReadableStreamTag__tagged` ->
`object->inherits<JSReadableStream>()` (guarded today, but one boolean
away).
Calling `classInfo()` on any cell while the mutator is sweeping is
forbidden: the cell's `Structure` may already have been swept. Assert
builds catch it; release builds corrupt the heap.
### Fix
Thread a `from_finalizer` flag through `ignore_remaining_response_body`.
When `true` (the `on_response_finalize` caller) skip `detach_js()` and
`clear_stream_handlers()`; only native state is touched. The sink's
JS-side detach still happens from `clear_sink()` in
`FetchTasklet::deinit()`, which runs as an event-loop `ConcurrentTask`
outside any sweep, so nothing leaks.
The `on_stream_cancelled_callback` caller (reader `.cancel()`, runs from
JS on the event loop) passes `false` and keeps the immediate detach.
Also corrects the `ResumableSink::detach_js` doc comment that claimed
finalizer safety.
### Verification
New test at `test/js/web/fetch/fetch-response-finalizer-sweep.test.ts`:
a child process under `BUN_JSC_collectContinuously=1` does 12 iterations
of `fetch()` with a user-constructed `ReadableStream` body (so the sink
takes the JS route with a Strong `js_this`) against a raw TCP server
that sends headers + a partial chunked body and never terminates it,
then drops the `Response` unconsumed and runs `Bun.gc(true)`.
Without the fix (`bun bd`, src/ stashed):
```
exitCode: 134
stderr: ASSERTION FAILED: vm().currentThreadIsHoldingAPILock() => vm().heap.mutatorState() != MutatorState::Sweeping
```
With the fix: `stdout: "ok"`, `exitCode: 0`.
`test/js/web/fetch/fetch-backpressure.test.ts` (exercises the
`on_stream_cancelled_callback` path) passes unchanged.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...type (oven-sh#32738)
### Repro
```js
using listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
await Bun.connect({
 hostname: "127.0.0.1",
 port: listener.port,
 socket: { open(s) { s.setTypeOfService({}); } },
});
```
On any assert build (debug, release-asan):
```
ASSERTION FAILED: isInt32()
 #4 JSC__JSValue__toInt32
 #5 TCPSocketPrototype__setTypeOfService
 #6 WebCore::TCPSocketPrototype__setTypeOfServiceCallback
```
On plain release the assert compiles out and the NaN-boxed bits of the
object handle are passed to `setsockopt(IP_TOS)` as the TOS byte.
### Cause
`set_type_of_service` in `src/runtime/socket/socket_body.rs` called
`args.ptr[0].to_int32()` on the raw argument. For non-numeric values
that falls through to the C++ `JSC__JSValue__toInt32`, which is
`JSC::JSValue::asInt32()` (the unchecked accessor that asserts
`isInt32()`), not a coercing conversion. The `node:net` wrapper
validates `tos` in JS before calling the handle, but the Bun-native
`Bun.connect` socket exposes this prototype method directly with no JS
validation layer.
### Fix
Route the argument through `validate_integer_range` with `min: 0, max:
255, field_name: "tos"`, the same pattern the sibling `setKeepAlive`
already uses for `initialDelay`. Non-numbers now throw
`ERR_INVALID_ARG_TYPE`, out-of-range integers throw `ERR_OUT_OF_RANGE`,
and non-integral numbers throw `ERR_INVALID_ARG_TYPE`, matching the
`node:net` surface.
I audited the other numeric setters on the TCPSocket/TLSSocket prototype
(`timeout`, `setMaxSendFragment`, `write` offset/length) and the
remaining `to_int32()` / `to_int64()` callers in `src/runtime/socket/`:
each is already gated by `is_number()` / `is_any_int()` or routes
through `coerce`. `setTypeOfService` was the only unguarded one.
### Verification
New test in `test/js/bun/net/socket.test.ts` spawns a subprocess that
calls `setTypeOfService` on a connected `Bun.connect` socket with `{}`,
`"x"`, `-1`, `256`, `1.5`, and `0x10`, and asserts the error code for
each plus that `getTypeOfService()` returns an integer. Without the fix
the subprocess aborts (exit 134) on the first call; with the fix all
seven checks pass. `test/js/node/test/parallel/test-net-socket-tos.js`
continues to pass.
Found by the bun-sys-fuzz API-grammar layer.
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...oven-sh#32743)
## What
`ReadableStream::from_pipe` (the `proc.stdout` / `proc.stderr` path for
`Bun.spawn` and the shell subprocess) moves an already-registered pipe
poll from the subprocess `PipeReader` into a freshly allocated
`NewSource<FileReader>` and re-points the poll's owner at it. The
across-read ref that keeps that box alive (`waiting_for_on_reader_done`
+ `increment_count()`, which upgrades `this_jsvalue` to `Strong`) was
only taken in `FileReader::on_start`, i.e. the first time JS actually
pulls from the stream.
Between `from_pipe` and that first pull, the poll's owner points into a
box whose only ref is the JS wrapper's own `Weak` back-reference. If the
`Subprocess` and its cached stdout become unreachable before anyone
pulls (a fire-and-forget spawn where `proc.stdout` is touched but never
read, and the direct child exits while something else still holds the
write end), GC sweeps the `JSFileInternalReadableStreamSource` wrapper
and frees the `NewSource<FileReader>` box while the poll is still armed.
The next readability or EOF event dispatches into freed memory:
```
READ of size 8 (heap-use-after-free)
 #0 Vec::len / is_empty (freed Vec<u8>)
 #2 webcore::file_reader::FileReader::on_reader_done FileReader.rs:1008
 #3 bun_io::pipe_reader::read_socket{closure} PipeReader.rs:846
 #4 PosixBufferedReader::read_socket PipeReader.rs:576
 #5 file-poll dispatch <- posix_event_loop <- us_internal_dispatch_ready_polls
freed by: JSC::JSDestructibleObjectDestroyFunc <- MarkedBlock sweep <- MarkedSpace::sweepBlocks
allocated: ReadableStream::from_pipe<subprocess::PipeReader> -> NewSource<FileReader>
```
Found by a coverage-guided GC-stress fuzzer with syscall interposition
(`BUN_JSC_collectContinuously=1` plus an injected `EAGAIN` to keep the
read pending). In release builds this is silent heap corruption.
## Fix
Take the across-read ref in `from_pipe` itself, immediately after the
live reader is transferred and the JS wrapper is created, so the box is
`Strong`-rooted for as long as the poll can fire. `on_reader_done` /
`on_reader_error` release it exactly as before. `FileReader::on_start`
now checks `waiting_for_on_reader_done` before taking the ref so the
later `handle.start()` call from `lazyLoadStream` does not double-count
on this path.
## How did you verify your code works?
The test asserts the lifetime invariant directly via
`heapStats().objectTypeCounts.FileInternalReadableStreamSource` rather
than racing for the crash, since the exact UAF trigger depends on the
fuzzer's syscall interposition. A detached grandchild (`sh -c 'while [ !
-e FLAG ]; do sleep 0.02; done; echo x'`) inherits the child's stdout
and keeps the write end open past the direct child's exit, so the
`FileReader`'s poll is still armed while we force GC with nothing in JS
referencing the wrapper.
- **Before** (`git stash push -- src/` + `bun bd test`): `duringLivePipe
= 0` of 4; every wrapper swept while its poll owner still points into
the freed box.
- **After**: `duringLivePipe >= 4`; once the grandchildren exit and the
pipes EOF, `afterEof <= 1` (one may remain via a conservatively-rooted
final `Subprocess`, same caveat as `spawn-ipc-gc.test.ts`).
Also passes `spawn-streaming-stdout.test.ts`,
`spawn-unread-stdout-gc.test.ts`, `spawn-ipc-gc.test.ts`,
`spawn-stdout-iterate-leak.test.ts`, and
`readablestream-helpers.test.ts`.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...ll-driven read (oven-sh#32986)
## Problem
Heap use-after-free in Bun Shell when `epoll_ctl` fails while
re-registering a pipe's `FilePoll` from a poll-driven read. Found by
syscall-fault-injection fuzzing against `origin/main`. Follow-up to
oven-sh#32754, which fixed the same failure on the eager spawn-time read path.
```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 1, thread T0
 #0 <bun_io::pipe_reader::BufferedReaderVTable>::link io/PipeReader.rs:105
 #1 <bun_io::pipe_reader::BufferedReaderVTable>::on_read_chunk io/PipeReader.rs:125
 #2 <bun_io::pipe_reader::PosixBufferedReader>::read_with_fn io/PipeReader.rs:890
 #3 <bun_io::pipe_reader::PosixBufferedReader>::read_socket io/PipeReader.rs:576
 #4 <bun_io::pipe_reader::PosixBufferedReader>::on_poll io/PipeReader.rs:529
 #5 __bun_run_file_poll runtime/dispatch.rs:677
freed by:
 <alloc::sync::Arc<bun_runtime::shell::subproc::PipeReader>>::drop
```
## Repro
1. `PipeReader::start` registers the poll and the eager spawn-time
`read_all()` hits `EAGAIN`, so `read_with_fn`'s `EAGAIN` arm
re-registers the poll and the spawn returns.
2. The child writes to stdout and the poll fires.
`__bun_run_file_poll`'s `BUFFERED_READER` arm dispatches straight into
`PosixBufferedReader::on_poll` with a bare `&mut *h` and no keepalive.
3. `read_with_fn` drains the chunk, `recv()` returns a real `EAGAIN`,
and `register_poll()` issues another `epoll_ctl`, which fails (`ENOMEM`
in the repro).
4. `register_poll` dispatches `on_reader_error`. The shell
`PipeReader::on_reader_error` signals the `Cmd`, the `Readable::Pipe`
`Arc` is dropped, and the callback's own `guard_from_raw` keepalive
becomes the last reference. The code already documents this: "Dropping
`guard` is the matching `deref()`; may free `this`."
5. Back in `read_with_fn`, the `EAGAIN` arm still delivers the drained
head: `parent.vtable.on_read_chunk(.., ReadState::Drained)` reads the
freed vtable.
Traced with the test's `LD_PRELOAD` shim:
```
[shim] epoll_ctl(ADD fd=13) unix call#1 -> ok PipeReader::start
[shim] recv(fd=13) unix call#1 -> EAGAIN eager read, inside spawn
[shim] epoll_ctl(MOD fd=13) unix call#2 -> ok re-register; spawn returns
[shim] recv(fd=13) unix call#2 poll fired: the child's bytes
[shim] recv(fd=13) unix call#3 real EAGAIN
[shim] epoll_ctl(MOD fd=13) unix call#3 -> ENOMEM register_poll fails
[shell_subproc] PipeReader(0x..250) onReaderError errno: 12
[shell_subproc] PipeReader(0x..250, stdout) detach()
[shell_subproc] PipeReader(0x..250, stdout) deinit()
==ERROR: AddressSanitizer: heap-use-after-free
```
## Cause
`register_poll()`'s failure path dispatches `on_reader_error`, which the
`BufferedReaderParent` contract explicitly allows to free the parent,
but `register_poll` gave the caller no way to know that happened.
`read_with_fn`'s `EAGAIN` arm is the only call site that touches the
reader afterwards; every other `register_poll()` is in tail position.
The `SAFETY` comment above the `parent` rebind claimed the parent is
"never freed mid-call", which holds for `on_read_chunk` re-entry but not
for `on_reader_error`.
oven-sh#32754 covered this exact sequence on the eager spawn-time entry by
holding an `Arc<PipeReader>` across `start()` and `read_all()` in
`Readable::start_pipe_reader`. The epoll dispatch has no equivalent
keepalive, so the poll-driven entry was still exposed.
## Fix
`PosixBufferedReader::register_poll()` now returns whether registration
succeeded. `false` means `on_reader_error` was dispatched and `self`
must not be touched again, so `read_with_fn`'s `EAGAIN` arm returns
there instead of delivering the drained head to a possibly freed parent.
The stream has already been completed with the registration error at
that point, so nothing is lost. All other `register_poll()` call sites
are tail calls and discard the result.
## Test
Two new modes in `test/js/bun/shell/shell-pipe-read-fault.test.ts`'s
`LD_PRELOAD` fault shim:
- `SHELL_RECV_EAGAIN_FIRST=1`: the first `recv()` on each `AF_UNIX`
socket returns `EAGAIN`, pushing the first successful read off the eager
spawn-time `read_all()` and onto the epoll dispatch.
- `SHELL_FAIL_EPOLL_FROM=N`: the Nth and later `epoll_ctl` `ADD`/`MOD`
on each `AF_UNIX` socket fail with `ENOMEM`. `N=3` lets the initial
registration and the eager read's re-registration succeed, then fails
the first poll-driven one.
The new test is `skipIf(!isASAN)` because the use-after-free is only
reliably observable under ASAN. With `src/io/PipeReader.rs` reverted to
`main` it fails in ~1.1s with the `heap-use-after-free` above; with the
fix all 6 tests in the file pass.
## Out of scope
Shell `PipeReader::on_read_chunk` also calls
`self.reader.register_poll()` from a `&mut self` method whose stated
contract is that it never frees `self`. If that inner registration
fails, the same free can happen under `read_with_fn`'s mid-loop flush
instead of its `EAGAIN` arm. Reaching it needs a large (>32 KB) burst in
one poll wake; I have not reproduced it, so it is not changed here.
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...ed (oven-sh#33016)
A backend message that fails the connection can share a TCP read with
messages that follow it. `PostgresRequest::on_data`'s message loop had
no bail-out once `fail()` had run, so the trailing messages in that read
kept being dispatched against the already-failed connection.
### Repro
A mock backend that answers the StartupMessage with one write carrying
two messages:
```
R int32(8) int32(99) Authentication, unrecognized type
Z int32(5) 'I' ReadyForQuery
```
```ts
const sql = new SQL({ url: `postgres://u@127.0.0.1:${port}/db`, max: 1, idleTimeout: 1, connectionTimeout: 5 });
await sql`select 1`.catch(() => {});
await Bun.sleep(1600);
```
### Cause
The unrecognized `Authentication` type calls `fail()`, which sets the
status to `Failed`, closes the socket, and rejects the pending requests,
but the message loop keeps going and dispatches the `ReadyForQuery` from
the same read. That calls `set_status(Status::Connected)`, which has no
guard against leaving `Failed`, so the dead connection is flipped back
to `Connected` and the `on_data` epilogue re-arms its idle timer.
uSockets frees a closed `us_socket_t` at the end of the event-loop
iteration, so when the timer later fires, `ref_and_close` reads the
freed socket:
```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 1 at 0x71f2125605d2 thread T0
 #0 us_socket_is_closed packages/bun-usockets/src/socket.c:143:21
 #4 PostgresSQLConnection::ref_and_close src/sql_jsc/postgres/PostgresSQLConnection.rs:1528:31
 #5 PostgresSQLConnection::fail_with_js_value src/sql_jsc/postgres/PostgresSQLConnection.rs:726:14
 #6 PostgresSQLConnection::fail_fmt src/sql_jsc/postgres/PostgresSQLConnection.rs:749:14
 #7 PostgresSQLConnection::on_connection_timeout src/sql_jsc/postgres/PostgresSQLConnection.rs:557:14
 #8 __bun_fire_timer src/runtime/dispatch.rs:1020:35
0x71f2125605d2 is located 18 bytes inside of 104-byte region
freed by thread T0 here:
 #2 us_internal_free_closed_sockets packages/bun-usockets/src/loop.c:305:9
```
### Fix
- `PostgresRequest::on_data`: the message loop returns once the
connection's status is `Failed`. `fail()` is terminal; nothing after it
in the same read should be handled (a `DataRow`, `CommandComplete`, or
`ErrorResponse` in that position would be just as wrong as the
`ReadyForQuery`).
- `PostgresSQLConnection::set_status`: refuses to transition out of
`Failed`. The transition function owns that invariant; every other
consumer of `Status` (the timer interval, `update_has_pending_activity`,
the idempotency check in `fail_with_js_value`) already assumes `Failed`
is terminal.
### Verification
`test/js/sql/postgres-failed-connection-resurrection.test.ts` runs a
fixture against the mock backend above and lets it outlive the
idle-timer window. Without the fix the fixture dies with the ASan report
above; with it the fixture exits 0. Gated to ASan builds because the bug
is a read of freed memory, which release lanes do not detect.
The postgres fault-injection and integration suites still pass locally
(90 tests across `test/js/sql/postgres-*.test.ts`, `sql*.test.ts`,
`tls-sql.test.ts`).
### Related
- oven-sh#32861 detaches the stored socket handle in `on_close` /
`on_connect_error` so nothing can dereference the freed `us_socket_t`
regardless of how the stale read is reached. It removes the last step of
this chain from the other end; this PR stops the failed connection from
being resurrected at all.
- oven-sh#30950 guards the JS pool's `handleConnected` against the reverse
ordering within one read (a legitimately queued `onconnect` microtask
arriving after a synchronous `onclose`).
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...h#33186)
### Repro
```sh
printf '{"name":"x","version":"1.0.0"}' > package.json
bun pm pkg set 'contributors[0]=alice'
```
On a release build (1.4.0 and current `main`) this exits 0 and writes
freed heap bytes into `package.json` as the property key:
```json
{
"name": "x",
"version": "1.0.0",
"P\x01\x00\x00\x00tors": {
 "\x00": "alice"
}
}
```
Depending on what was in the freed allocation the result is often not
valid JSON at all. Any `bun pm pkg set` key path containing `[index]`
hits it.
Under ASAN it is a deterministic `heap-use-after-free`:
```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 1
 #0 bun_js_printer::write_pre_quoted_string_inner src/js_printer/lib.rs:1014
 #7 PmPkgCommand::save_package_json src/runtime/cli/pm_pkg_command.rs:909
freed by thread T0 here:
 #7 <Box<[u8]> as Drop>::drop
 #12 PmPkgCommand::set_value src/runtime/cli/pm_pkg_command.rs:661
previously allocated by thread T0 here:
 #10 <Box<[u8]> as From<&[u8]>>::from
 #11 PmPkgCommand::parse_key_path src/runtime/cli/pm_pkg_command.rs:583
```
<details>
<summary>full ASAN report</summary>
```
=================================================================
==16563==ERROR: AddressSanitizer: heap-use-after-free on address 0x73423c7c0670 at pc 0x00000f583cc5 bp 0x7fff2667e950 sp 0x7fff2667e948
READ of size 1 at 0x73423c7c0670 thread T0
 #0 0x00000f583cc4 in _RINvCs59Hqei94dXF_14bun_js_printer29write_pre_quoted_string_innerINtB2_16StdWriterAdapterQINtB2_6WriterNtB2_12BufferWriterEEKVNtNtB2_8Encoding4Utf8UECsgBGN0jRPILJ_11bun_bundler /workspace/bun/src/js_printer/lib.rs:1014:79
 #1 0x00000ebda439 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_characters_utf8 /workspace/bun/src/js_printer/lib.rs:2641:21
 #2 0x00000ebdb7a7 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_characters_e_string /workspace/bun/src/js_printer/lib.rs:4546:22
 #3 0x00000ebdb238 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_literal_e_string /workspace/bun/src/js_printer/lib.rs:3018:18
 #4 0x00000ebd2be2 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_property /workspace/bun/src/js_printer/lib.rs:4807:34
 #5 0x00000ebc0166 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_expr /workspace/bun/src/js_printer/lib.rs:3962:38
 #6 0x00000ee574c1 in bun_js_printer::print_json::<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>> /workspace/bun/src/js_printer/lib.rs:8071:13
 #7 0x00000c05c270 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::save_package_json /workspace/bun/src/runtime/cli/pm_pkg_command.rs:909:25
 #8 0x00000c060cfb in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:330:13
 #9 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32
 #10 0x00000bf919d0 in <bun_runtime::cli::package_manager_command::PackageManagerCommand>::exec /workspace/bun/src/runtime/cli/package_manager_command.rs:704:13
 #11 0x00000c3fbb87 in bun_runtime::cli::command::exec_pm /workspace/bun/src/runtime/cli/mod.rs:1591:34
 #12 0x00000c3f2b86 in bun_runtime::cli::command::start /workspace/bun/src/runtime/cli/mod.rs:1309:43
 #13 0x00000bfad16c in bun_runtime::cli::cli::start /workspace/bun/src/runtime/cli/mod.rs:573:27
 #14 0x00000bb3c034 in main /workspace/bun/src/bun_bin/lib.rs:230:5
 #15 0x77223ccc7ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
 #16 0x77223ccc7d64 in __libc_start_main csu/../csu/libc-start.c:360:3
 #17 0x0000099d1d1d in __wrap___libc_start_main /workspace/bun/build/debug/../../src/jsc/bindings/workaround-missing-symbols.cpp:487:12
0x73423c7c0670 is located 0 bytes inside of 12-byte region [0x73423c7c0670,0x73423c7c067c)
freed by thread T0 here:
 #0 0x000007ae192a in free crtstuff.c
 #1 0x00000bb3c5a7 in <std::alloc::System as core::alloc::global::GlobalAlloc>::dealloc /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/alloc/unix.rs:48:18
 #2 0x00000bb3be9a in __rustc::__rust_dealloc /workspace/bun/src/bun_bin/lib.rs:56:15
 #3 0x00001258b05f in alloc::alloc::dealloc_nonnull /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:128:14
 #4 0x0000125872fe in <alloc::alloc::Global>::deallocate_impl_runtime /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:229:22
 #5 0x000012586364 in <alloc::alloc::Global>::deallocate_impl /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:344:9
 #6 0x00001258d79c in <alloc::alloc::Global as core::alloc::Allocator>::deallocate /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:462:23
 #7 0x000012582946 in <alloc::boxed::Box<[u8]> as core::ops::drop::Drop>::drop /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1956:24
 #8 0x000012572e44 in core::ptr::drop_in_place::<alloc::boxed::Box<[u8]>> /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1
 #9 0x000011f8d429 in core::ptr::drop_in_place::<[alloc::boxed::Box<[u8]>]> /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1
 #10 0x00000ef6b73a in <alloc::vec::Vec<alloc::boxed::Box<[u8]>> as core::ops::drop::Drop>::drop /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:4258:13
 #11 0x00000ef69e64 in core::ptr::drop_in_place::<alloc::vec::Vec<alloc::boxed::Box<[u8]>>> /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1
 #12 0x00000c061846 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::set_value /workspace/bun/src/runtime/cli/pm_pkg_command.rs:661:5
 #13 0x00000c061038 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:325:13
 #14 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32
 #15 0x00000bf919d0 in <bun_runtime::cli::package_manager_command::PackageManagerCommand>::exec /workspace/bun/src/runtime/cli/package_manager_command.rs:704:13
 #16 0x00000c3fbb87 in bun_runtime::cli::command::exec_pm /workspace/bun/src/runtime/cli/mod.rs:1591:34
 #17 0x00000c3f2b86 in bun_runtime::cli::command::start /workspace/bun/src/runtime/cli/mod.rs:1309:43
 #18 0x00000bfad16c in bun_runtime::cli::cli::start /workspace/bun/src/runtime/cli/mod.rs:573:27
 #19 0x00000bb3c034 in main /workspace/bun/src/bun_bin/lib.rs:230:5
 #20 0x77223ccc7ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
previously allocated by thread T0 here:
 #0 0x000007ae1bc8 in malloc crtstuff.c
 #1 0x00000bb3c520 in <std::alloc::System as core::alloc::global::GlobalAlloc>::alloc /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/alloc/unix.rs:14:22
 #2 0x00000bb3be30 in __rustc::__rust_alloc /workspace/bun/src/bun_bin/lib.rs:56:15
 #3 0x00001258b335 in alloc::alloc::alloc /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:101:9
 #4 0x000012586b81 in <alloc::alloc::Global>::alloc_impl_runtime /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:210:73
 #5 0x0000125862b6 in <alloc::alloc::Global>::alloc_impl /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:332:9
 #6 0x00001258d86a in <alloc::alloc::Global as core::alloc::Allocator>::allocate /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:449:14
 #7 0x00001257dbd3 in <alloc::boxed::Box<[u8]>>::try_clone_from_ref_in /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:881:29
 #8 0x00001257da49 in <alloc::boxed::Box<[u8]>>::clone_from_ref_in /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:840:15
 #9 0x00001257d3f4 in <alloc::boxed::Box<[u8]>>::clone_from_ref /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:793:9
 #10 0x000012581e34 in <alloc::boxed::Box<[u8]> as core::convert::From<&[u8]>>::from /root/.rustup/toolchains/nightly-2026年05月06日-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed/convert.rs:77:9
 #11 0x00000c05a47f in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::parse_key_path /workspace/bun/src/runtime/cli/pm_pkg_command.rs:583:37
 #12 0x00000c061608 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::set_value /workspace/bun/src/runtime/cli/pm_pkg_command.rs:643:30
 #13 0x00000c061038 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:325:13
 #14 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32
```
</details>
### Cause
`parse_key_path` returned a `Vec<Box<[u8]>>`, and `set_value` /
`set_nested` inserted those boxed segments into the manifest AST by
reference: `E::Object::put` constructs `EString::init(key)`, whose
documented contract is that `key` is arena-owned (it records the slice,
it does not copy it). The vector is a local of `set_value`, so it
dropped before `exec_set` reached `save_package_json`, and the JSON
printer then read the dangling keys.
The non-bracket path in `set_value` did not have the bug: it borrowed
its segments straight out of the argv key, which outlives the whole
command. The bracket path differed only by the unnecessary boxing.
### Fix
`parse_key_path` now returns `Vec<&[u8]>`. Every segment is a literal
sub-slice of the input key, so nothing ever needed owning. With the
boxing gone, `set_value`'s separate non-bracket branch and its
`set_nested_simple` helper (which existed only to avoid the allocation)
were exact duplicates of the bracket path, so they are deleted and all
keys route through `parse_key_path` + `set_nested`.
`set_nested_simple`'s trailing `root.put(current_key, nested)` was a
no-op: `ExprData::EObject` is a `StoreRef` handle, so mutating the copy
returned by `root.get()` already mutates the stored object, and the put
re-stores the same handle. Dropping it with the function changes nothing
(and the prior bracket path, `set_nested`, never had it).
Intentionally not changed here: `set 'contributors[0]=alice'` produces
`"contributors": {"0": "alice"}`, an object keyed by the digit string,
rather than the array npm's `pkg set` creates, and `set 'array[]=x'`
still errors with `InvalidPath` instead of appending. Both are the npm
compat gap tracked in oven-sh#22035, which is separate from the memory safety
of the key names and is not closed by this PR.
### Verification
New test in `test/cli/install/bun-pm-pkg.test.ts` reparses the written
file and asserts the exact object. Without the fix it fails on release
(`SyntaxError: JSON Parse error: Invalid escape character x`) and on the
ASAN debug build (the child aborts on the use-after-free). With the fix
the full `bun-pm-pkg.test.ts` suite passes (74 pass, 0 fail).
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...en-sh#33242)
### What
After a 3xx redirect, `handle_response_metadata` rewrites per-hop
request state on the HTTP-thread clone of the `AsyncHTTP`:
- `client.url` (and `connected_url`) become a self-borrow into
`client.redirect`, a `Vec<u8>` the clone owns and frees in the
final-callback teardown (`AsyncHTTP::on_async_http_callback_raw`).
- On a cross-origin hop,
`Authorization`/`Proxy-Authorization`/`Cookie`/`Host` are removed from
`client.header_entries` in place.
- The method may be downgraded to GET.
`NetworkTask::notify`'s bitwise copy-back (`ptr::write(real,
ptr::read(async_http))`) carries all of that into the JS-thread
`AsyncHTTP`. When `bun install` retries the task after a retryable
failure (5xx or a connection reset on the redirect target), the
re-scheduled request therefore:
1. connects through the freed redirect buffer (use after free), and
2. if the redirect was cross-origin, goes out without `Authorization`,
so an authorized registry answers 401.
ASAN (debug build), deterministic on the first try:
```
ERROR: AddressSanitizer: heap-use-after-free ... thread T1 (HTTP Client)
READ of size 1
 #0 bun_core::fmt::parse_int::<u16> src/bun_core/fmt.rs:929
 #1 <bun_url::URL>::get_port src/url/lib.rs:470
 #2 <bun_url::URL>::get_port_auto src/url/lib.rs:474
 #3 <bun_http::http_thread::HttpThread>::connect src/http/HTTPThread.rs:602
 #4 <bun_http::HTTPClient>::start_ src/http/lib.rs:2635
 #6 <bun_http::async_http::AsyncHTTP>::on_start src/http/AsyncHTTP.rs:893
freed by thread T1 (HTTP Client):
 <bun_http::async_http::AsyncHTTP>::on_async_http_callback_raw src/http/AsyncHTTP.rs:774
previously allocated by thread T1 (HTTP Client):
 <bun_http::HTTPClient>::handle_response_metadata src/http/lib.rs:5038
```
On a release build the same sequence does not crash, but the retries
never reach the server (each one connects through freed memory) and the
install fails.
### Repro
A scripted registry where the manifest URL 302-redirects and the
redirect target answers a 500 once, then the real packument:
```
GET /BaR -> 302 Location: /redirected/BaR
GET /redirected/BaR -> 500 on the first hit, then the packument
GET /BaR-0.0.2.tgz -> tarball
```
`bun install` against it aborts under ASAN and fails on release. Any
301/302/307/308 and 1- or 2-hop chains hit the same path. With an
authorized registry that redirects cross-origin (the common Artifactory
/ CodeArtifact / GitHub Packages shape), the retry also loses
`Authorization`; that variant fails with `GET <registry>/BaR - 401` even
once the URL is fixed.
### Fix
`src/http/AsyncHTTP.rs`: the `!has_more` teardown block already releases
every clone-owned allocation. Before freeing `client.redirect`, restore
the per-hop state that a re-scheduled attempt must not inherit:
- `client.url` back to the caller-owned pre-redirect URL
(`AsyncHTTP.url`, which borrows memory valid for the original's whole
lifetime), and `client.connected_url` (which `connect` derives from it)
to default.
- `client.header_entries` back to the untouched
`AsyncHTTP.request_headers`. The list is bitwise-shared with the
JS-thread original, so it must not be dropped or reallocated on the HTTP
thread; it was cloned from `request_headers` at init and only ever
shrinks, so `clear_retaining_capacity()` +
`append_list_assume_capacity()` restores it in place.
- `client.method` back to `AsyncHTTP.method`.
Nothing that crosses back to the JS thread references clone-freed memory
anymore, and a retried request restarts from the original URL with the
original headers instead of the last redirect hop's, which is what the
install-level retry is meant to do.
### Tests
`test/cli/install/bun-install-retry.test.ts`:
- `retries a manifest whose redirect target 500s once`
- `retries a tarball whose redirect target 500s once` (the sibling retry
site in `runTasks`)
- `retries an authorized manifest whose cross-origin redirect target
500s once` (also asserts the cross-origin hop itself still does NOT
carry `Authorization`, so the spec-mandated strip is unchanged)
All three fail on the unfixed build (ASAN abort under `bun bd`, install
error with `USE_SYSTEM_BUN=1`). The third additionally fails with a 401
if only the URL is restored and not the headers, so each restore is
load-bearing. `test/js/web/fetch/fetch-redirect.test.ts` and
`fetch-url-after-redirect.test.ts` still pass, so `response.url` after a
redirect is unaffected (it comes from the owned `metadata.url` copy, not
from `client.url`).
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
sroussey pushed a commit that referenced this pull request Jul 17, 2026
...buffer cannot be allocated (oven-sh#33326)
Fixes a `Segmentation fault at address 0x00000040` (sometimes
`0x00000030`) reported from Windows x64 builds, crashing inside
boringssl's record copy from uSockets' TLS read loop:
```
memcpy src/vctools/crt/vcruntime/src/string/amd64/memcpy.asm
bssl::OPENSSL_memcpy vendor/boringssl/crypto/internal.h:868
SSL_peek vendor/boringssl/ssl/ssl_lib.cc:947
SSL_read vendor/boringssl/ssl/ssl_lib.cc:918
us_internal_ssl_on_data packages/bun-usockets/src/crypto/openssl.c:1797
us_internal_dispatch_ready_poll packages/bun-usockets/src/loop.c:600
uv__fast_poll_process_poll_req vendor/libuv/src/win/poll.c:208
uv_run vendor/libuv/src/win/core.c:737
```
## Cause
`us_internal_init_loop_ssl_data` (`openssl.c:677`) allocates one 512 KiB
plaintext buffer per event loop, lazily, on the loop's first TLS socket,
and never checked the result:
```c
loop_ssl_data->ssl_read_output =
 us_malloc(LIBUS_RECV_BUFFER_LENGTH + LIBUS_RECV_BUFFER_PADDING * 2);
```
With `ssl_read_output == NULL`, every later `SSL_read` hands boringssl
```c
loop_ssl_data->ssl_read_output + LIBUS_RECV_BUFFER_PADDING + read
```
as its plaintext destination, so the first record of application data
memcpy's to `NULL + 32`. `SSL_peek`'s `OPENSSL_memcpy(buf, ...)` at
`ssl_lib.cc:947` is the write, and the access violation confirms it is a
write fault.
`0x30`/`0x40` rather than `0x20` is memcpy's destination-alignment
preamble (`dst += VEC_SIZE; dst &= ~(VEC_SIZE - 1)`), which moves the
first faulting store for copies larger than eight vector registers.
Measured on the copy sizes a real TLS record produces:
| memcpy variant | first faulting store for `dst = NULL + 32` |
| --- | --- |
| 32-byte vectors (AVX) | `0x40` |
| 16-byte vectors (SSE) | `0x30` |
So the two strikingly stable fault addresses are just CPU dispatch
across the affected machines, and `read` is always `0`: the crash is
always the connection's first record of application data.
Only Windows reports it because Linux and macOS overcommit, so a 512 KiB
`malloc` there effectively never returns NULL. Windows fails the commit
cleanly, and the loop's much smaller `us_calloc` still succeeds out of
an already-committed page, leaving exactly the observed shape: a valid
`loop_ssl_data` whose `ssl_read_output` is NULL.
## Fix
- Null-check the buffer allocation, the `us_calloc` of `loop_ssl_data`,
and the `BIO_meth_new`/`BIO_new` calls beside them, and route the
failure through Bun's out-of-memory crash path (`Bun__outOfMemory`, new
C entry point next to `Bun__panic`). The process now dies with `Bun ran
out of memory` and a stack trace that names the allocation, instead of
faulting on the first TLS byte.
- Apply the same check to the sibling site: `recv_buf`/`send_buf` in
`us_internal_loop_data_init` are the same unchecked
`malloc(LIBUS_RECV_BUFFER_LENGTH + LIBUS_RECV_BUFFER_PADDING * 2)`. A
NULL `recv_buf` does not fault, it makes every read on the loop fail
with `EFAULT` for the life of the process, which is worse to diagnose.
- `us_internal_free_loop_ssl_data` left `loop->data.ssl_data` dangling,
which defeats the `if (!loop->data.ssl_data)` guard the init function
relies on. It now clears the field.
A 512 KiB `malloc` effectively never returns NULL on an overcommitting
kernel, so the failure path needs the existing socket fault injector to
be reachable from a test. This adds an `ssl_loop_buffer` rule to it,
which like the rest of the injector is compiled out of release builds.
## Verification
The new test spawns a child that arms `ssl_loop_buffer` before its first
TLS socket and asserts it reports out of memory rather than reaching a
read loop.
Reverting only `if (!loop_ssl_data->ssl_read_output)
Bun__outOfMemory();` reproduces the reported crash exactly, on Linux,
from that same fixture: same fault address, same frames, same boringssl
source lines.
<details>
<summary>Reproduction on the unfixed build (<code>bun bd</code>,
ASAN)</summary>
```
==19592==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000040
==19592==The signal is caused by a WRITE memory access.
==19592==Hint: address points to the zero page.
 #0 __memcpy_evex_unaligned_erms
 #1 bssl::OPENSSL_memcpy(void*, void const*, unsigned long) vendor/boringssl/crypto/internal.h:868:10
 #2 SSL_peek vendor/boringssl/ssl/ssl_lib.cc:947:3
 #3 SSL_read vendor/boringssl/ssl/ssl_lib.cc:918:13
 #4 us_internal_ssl_on_data packages/bun-usockets/src/crypto/openssl.c:1847:21
 #5 us_internal_dispatch_ready_poll packages/bun-usockets/src/loop.c:625:38
```
With the fix:
```
panic(main thread): Bun ran out of memory
Bun__outOfMemory src/bun_bin/phase_c_exports.rs:81:5
us_internal_init_loop_ssl_data packages/bun-usockets/src/crypto/openssl.c:696:42
us_internal_ssl_attach packages/bun-usockets/src/crypto/openssl.c:1275:3
```
</details>
`test/js/node/tls/tls-syscall-fault.test.ts` (11 pass),
`test/js/bun/util/socket-fault-injection.test.ts` (15 pass), plus
`socket-syscall-fault`, `serve-syscall-fault` and `fetch-syscall-fault`
(19 pass) are green. The three failures in `test/js/node/tls/` on this
machine are pre-existing: two also fail on an unmodified 1.4.0, and
`tls.connect should ignore invalid NODE_EXTRA_CA_CERTS` takes 5.75s,
just over the 5s local default (CI triples the per-test timeout for ASAN
builds).
## Teardown audit
The report also asked whether a socket can reach
`us_internal_ssl_on_data` after its loop's SSL data has been freed.
`us_internal_free_loop_ssl_data` is only reachable from `us_loop_free`,
and the only loop Bun frees today is `SpawnSyncEventLoop`'s, which never
has a TLS socket attached (its `loop->data.ssl_data` is always NULL, so
the free is a no-op). So that is not the cause here.
It is worth noting separately that the libuv `us_loop_free`
(`eventing/libuv.c:201-206`) calls `us_internal_loop_data_free(loop)`
and *then* runs `uv_run(loop->uv_loop, UV_RUN_NOWAIT)`, which is a full
libuv iteration that can dispatch socket poll callbacks into the
just-freed `recv_buf` and `ssl_data`. The POSIX `us_loop_free`
(`eventing/epoll_kqueue.c:56-60`) has no such window. It is unreachable
today for the reason above, so it is left out of this PR rather than
changing loop teardown ordering without a test that can exercise it.
sroussey pushed a commit that referenced this pull request Jul 17, 2026
... server (oven-sh#34024)
`test/integration/next-pages/test/dev-server-ssr-100.test.ts` was
reported RED in build
[72106](https://buildkite.com/bun/bun/builds/72106) on `:darwin: 26
aarch64` with `5 crashes reported during this test`. The test itself is
fine on main (last four completed main builds 72110/72020/71943/71860
have no mention of it). The RED was manufactured by CI's crash-report
attribution.
## Cause
`scripts/runner.node.mjs` exports
`BUN_CRASH_REPORT_URL=http://localhost:<remapPort>` to every test so
real crashes are captured. It only drains `/traces` when a test exits
non-zero (the known caveat is documented in the runner at the drain
site).
`run-crash-handler.test.ts` spawns processes that crash on purpose with
`env: bunEnv`, which inherits that URL, and `native-plugin.test.ts`'s
"prints name when plugin crashes" does the same via `Bun.$`. Both files
pass (exit 0), so their five crash reports stay on the remap server:
```
Segmentation fault at address 0x00000000 # native-plugin
panic: invoked crashByPanic() handler # run-crash-handler (x2)
Bun ran out of memory # run-crash-handler
Segmentation fault at address 0xDEADBEEF # run-crash-handler
```
In build 72106 a transient npm-registry hang on one tart agent
(`66790-tart-26`) made `dev-server-ssr-100`'s `bun i` block for 100 s
and time out. That non-zero exit drained `/traces`, inherited the five
deliberate crashes, and became `error = "crash reported"`;
`isAlwaysFailure("crash reported")` blocks retries, so one transient
timeout became a hard RED. `next-auth.test.ts` hit the same npm hang on
the same agent minutes later, got normal retries, and passed on attempt
#4.
Same five reports pinned on unrelated tests in other recent PR builds:
- build [72095](https://buildkite.com/bun/bun/builds/72095):
`test/js/node/http/node-http-backpressure-max.test.ts` ("5 crashes
reported", identical list)
- build [72085](https://buildkite.com/bun/bun/builds/72085):
`test/js/web/fetch/fetch-leak.test.ts` (segfault at `0x0` from
`native-plugin`)
## Fix
Set `BUN_CRASH_REPORT_URL=""` (and `BUN_ENABLE_CRASH_REPORTING=0` for
the fall-through branch in `is_reporting_enabled()`) on every spawn that
crashes on purpose but is not asserting on upload behaviour:
- `run-crash-handler.test.ts`: the three `env: bunEnv` spawns now use a
shared `noReportEnv`.
- `native-plugin.test.ts`: the "prints name when plugin crashes" `Bun.$`
command sets the two vars inline.
The "automatic crash reporter" and "raise ignoring panic handler" tests
already point `BUN_CRASH_REPORT_URL` at their own local server and are
unchanged.
## Verification
Simulated CI's remap server and ran the test file against it:
```
before: CI-server hits: 5 (the /ack uploads listed above)
after: CI-server hits: 0, 9 pass / 1 skip / 0 fail
```
`bun bd test test/cli/run/run-crash-handler.test.ts` passes.
`native-plugin.test.ts` "prints name when plugin crashes" is
`skipIf(isASAN)` so it is skipped under the debug build; a neighbouring
case in the same file still loads, and the inline-env override was
verified separately (`Bun.$\`VAR="" ...\`` reaches the child as an empty
string, which `is_reporting_enabled()` treats as disabled).
Test-only change; no `src/` diff because the behaviour being fixed lives
in the CI runner and the child's env, not in bun itself.
<!-- robobun:evidence:begin -->
---
**no test proof** · iteration 3 · Platform-specific test-only change;
deferring to CI.
<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

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