forked from oven-sh/bun
-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Conversation
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
...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 -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Updates lolhtml to version v2.2.0
Auto-updated by this workflow