forked from oven-sh/bun
-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Conversation
igorls
pushed a commit
that referenced
this pull request
Aug 21, 2026
...rier (oven-sh#36337) `JSNativeStreamSourceAdapter::m_controller` was a `JSC::Weak<JSReadableStreamDefaultController>`. When the native pull promise is rejected (socket fault on a fetch body) the adapter is queued as the `onNativePullRejected` reaction context, which roots the **adapter** but not the **controller**: the adapter's only edge to it was the `Weak`. `FetchTasklet` releases both native `Strong<>`s to the body stream before that microtask drains, so a GC in between can leave the entire consumer graph (`controller -> stream -> reader -> pipe op -> destination -> writer -> readyPromise`) white. The subsequent error cascade then enqueues the pipe's writes-drained shutdown deferral against a corpse `op`, and `performPipeShutdownAction(AbortDestination)` dereferences a swept `readyPromise`: ``` ASSERTION FAILED: result JSObject.h(583) JSGlobalObject *JSC::JSObject::realm() const #5 JSC::JSObject::realm() #6 JSC::JSPromise::rejectPromise #7 JSC::JSPromise::reject #8 Bun::WebStreams::writableStreamDefaultWriterEnsureReadyPromiseRejected #9 Bun::WebStreams::writableStreamStartErroring oven-sh#10 Bun::WebStreams::writableStreamAbort oven-sh#11 WebCore::performPipeShutdownAction (AbortDestination) oven-sh#12 WebCore::JSStreamPipeToOperation::onWritesFinishedForShutdown ``` On builds without the assert the same path is a silent write into freed/reused promise memory. ## Fix Hold `m_controller` as a visited internal field so a queued adapter roots the controller directly. The edge is cleared on every terminal path (`nativeSourcePullRejected`, `nativeSourceCallClose`, `nativeSourceCancel`); `controller->algorithmContext` is cleared by `readableStreamDefaultControllerClearAlgorithms`, so the abandoned case is an ordinary intra-heap cycle mark-sweep collects. `NewSource::this_jsvalue` is only `Strong` during FileReader I/O, where pinning the consumer graph is the correct behavior anyway. With the `Weak` gone the adapter no longer needs a destructor, so it is now a `JSInternalFieldObjectImpl<5>`: the five JSValue members (handle, pendingView, closer, drainValue, controller) are internal fields visited by the base class, with typed accessors at call sites. The scalar members (chunkSize, flag bitfield, text-decode state) stay as plain members. ## Verification `native-source-onclose-leak.test.ts` (the partial-read + `releaseLock` abandonment tests for Blob/fetch/File sources) continues to pass, confirming the cycle does not pin. `streams.test.js`, `pipeTo-signal-leak.test.ts`, `compression.test.ts`, `blob.test.ts` all pass. The crash itself is 0/1800 standalone; it reproduces ~1/3 only under a fault-injected tracer replay. `pipeTo-shutdown-gc.test.ts` exercises the shape (native body source, socket fault mid-stream, fire-and-forget `pipeTo` under `collectContinuously`, `AbortDestination` shutdown arm) as a regression surface. <!-- robobun:evidence:begin --> --- **no test proof** · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/streams/pipeTo-shutdown-gc.test.ts <!-- robobun:evidence:end -->
igorls
pushed a commit
that referenced
this pull request
Aug 21, 2026
...llback (oven-sh#36986) ## What `test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts` (the crypto-generateKeyPair fixture) fails on every Linux x64-asan run since oven-sh#36598 landed (builds [89023](https://buildkite.com/bun/bun/builds/89023), [89031](https://buildkite.com/bun/bun/builds/89031)): ``` direct leak of 24b in run (src/runtime/node/node_crypto_binding.rs:85:21) +34 more SUMMARY: AddressSanitizer: 1480 byte(s) leaked in 35 allocation(s). #6 EVP_PKEY_keygen vendor/boringssl/crypto/evp/evp_ctx.cc #7 Bun::KeyPairJobCtx::runTask src/jsc/bindings/node/crypto/CryptoGenKeyPair.cpp:23 #8 Bun__RsaKeyPairJobCtx__runTask src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp:26 ``` ## Cause The 11 extern crypto job ctxs (generateKeyPair x5, sign/verify, diffieHellman, hkdf, generatePrime, checkPrime, generateKey) completed by invoking the JS callback from inside C++ `runFromJS` while the ctx was still alive; the ctx was freed only after `then()` returned. A callback that never returns (the fixture calls `process.exit(0)` inside it) stranded everything the ctx still owned: the generated `EVP_PKEY`, `KeyObjectData` refs, `BIGNUM`s. The leak is pre-existing; oven-sh#36598 made it observable by routing `OPENSSL_malloc` through libc under ASAN. Whether LSan reported the other job types too was codegen luck (their pointers happened to be reachable by the conservative stack scan); `generateKeyPair`'s `EVP_PKEY` sits behind two FastMalloc indirections and was reported deterministically. ## Fix Make it structurally impossible for a job ctx to hold native resources across user JS: the native side never sees the callback. - `runFromJS` keeps its name (the JS-thread half, paired with the work-pool half `runTask`) but no longer receives the callback. It returns `JSCallbackArgs`, a small by-value type whose constructors are the only producers, so bodies read `return { err };` or `return { jsNull(), publicKey, privateKey };`. The extern "C" shims copy it through a typed out-pointer (C linkage cannot return a class type); the Rust side consumes it as a slice. - The Rust `extern_crypto_job!` plumbing does, in order: run `runFromJS` to produce the arguments, free the ctx (`ctx_deinit`), invoke the callback. The invariant lives in one place and applies to every job type. - Shutdown release: a completion task enqueued but not yet dispatched when `process.exit()` runs (exit racing the work pool) used to be re-queued at shutdown, stranding the ctx the same way. `AnyTaskJob` now carries an erased release entry and the shutdown release frees the job without running its completion. A completion posted after the final drain is not recoverable without joining the work pool (which would block exit); `test-crypto-op-during-process-exit.js` stays in `no-validate-leaksan.txt` for that sliver, now with an accurate comment. - The caught-export-exception paths encoded the `JSC::Exception` cell itself, so the callback's err argument was not the thrown Error (not `instanceof Error`, no `code`). They now use `Exception::value()`, matching node: JWK export of an unsupported curve surfaces `ERR_CRYPTO_JWK_UNSUPPORTED_CURVE`. No behavior change otherwise: - `Bun__EventLoop__runCallback{1,2,3}` were Rust's `EventLoop::run_callback` exported to C++. The plumbing now calls `run_callback` directly: same enter/exit bracketing, same pending-exception gate, same unhandled-exception reporting, same synchronous timing. This made `runCallback1`/`runCallback3` dead (the crypto bodies were their last callers), so their exports and declarations are deleted; `runCallback2` stays for the webview backends. - Callback arity is preserved per path (observable via `arguments.length`): error paths pass 1 arg, results 2, generateKeyPair success 3. - Exception paths are preserved: a throw out of argument production skips the callback and reports unhandled, as before. Each `runFromJS` checks its `ThrowScope` after every call that can throw (`RETURN_IF_EXCEPTION`), since the check that used to happen inside the nested `runCallbackN` call now happens after the C++ scope destructs; `BUN_JSC_validateExceptionChecks` verifies this on the asan lane. - The produced `JSValue`s live on the `then()` stack frame between production and invocation, which JSC's conservative scan covers; they are JS-heap values, so freeing the ctx first cannot invalidate them. - Perf: same number of FFI crossings, no allocation added. The Rust-native crypto jobs (pbkdf2, scrypt, random) already had the ordering property: they resolve promises or queue the callback via nextTick, so their ctx drops before user JS runs. The synchronous-callback extern jobs were the gap. ## Verification New tests in `crypto.key-objects.test.ts`: - `isASAN`-gated leak suite: children run with `BUN_DESTRUCT_VM_ON_EXIT=1` and `detect_leaks=1` (the asan lane's configuration) and call `process.exit(0)` from the callback of each job type: generateKeyPair (KeyObject and encrypted PEM outputs), sign, diffieHellman, hkdf, checkPrime, generateKey, plus an exit-before-completion-dispatch case (busy-spin so the queued completion is never dispatched). - An export-error test: `generateKeyPair('ec', { namedCurve: 'secp224r1', ...jwk encodings })` asserts the callback err is `instanceof Error` with code `ERR_CRYPTO_JWK_UNSUPPORTED_CURVE` (matches node; fails on main, which passes the Exception cell). Results: - unfixed build (src stashed): both generateKeyPair leak tests fail with the exact CI signature (`Direct leak of 24 byte(s)` in `EVP_PKEY_keygen` via `KeyPairJobCtx::runTask`) - fixed build: all pass, including under `BUN_JSC_validateExceptionChecks=1`, and ec/ed25519 keypair and verify probes run leak-clean as well - `AsyncLocalStorage-tracking.test.ts`: 74 pass, 0 fail (all async-context crypto fixtures, against both bun and node) - `crypto.test.ts` (369), `crypto.key-objects.test.ts` (117), and 37 node parallel files (`test-crypto-keygen*`, `test-crypto-sign-verify`, `test-crypto-hkdf`, `test-crypto-dh-stateless`, `test-crypto-*prime*`) all pass The break landed with oven-sh#36598 (which made the leak visible); oven-sh#36657 proposed clearing individual ctx fields before the callback, and this PR supersedes that approach with the ordering guarantee in the job plumbing instead of per-field resets. <!-- robobun:evidence:begin --> --- **no test proof** · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts test/js/node/crypto/crypto.key-objects.test.ts <!-- robobun:evidence:end -->
igorls
pushed a commit
that referenced
this pull request
Aug 21, 2026
...wo parallel vecs (oven-sh#39145) ### Problem - `LOLHTMLContext` in `src/runtime/api/html_rewriter.rs` keeps two vecs, `selectors` and `element_handlers`, that describe one thing: entry `i` of each is the selector and the handler object from the same `rewriter.on(selector, handlers)` call. - The pairing is only held up by convention: `on_()` pushes to both, `build_settings()` zips them back together, and a doc comment plus an invariant comment explain it. The mordant `parallel_vecs` lint flags this (the one baselined finding for this file). ### Fix - Add `ElementHandlerEntry { selector, handler: Box<ElementHandler> }` and store `element_handlers: Vec<ElementHandlerEntry>`. One vec, one push in `on_()`, and `build_settings()` destructures each entry instead of zipping. - No behavior change: the same values are pushed in the same order, the handler is still boxed (the lol-html closures built in `build_settings()` hold raw pointers into the box, so it must not move when the vec reallocates), and the body of the `build_settings()` loop is unchanged. The `#[expect(clippy::vec_box)]` comes off `element_handlers` because it is no longer a `Vec<Box<_>>`; `document_handlers` keeps its own. - Remove the `parallel_vecs:src/runtime/api/html_rewriter.rs` line from `mordant-baseline.toml`. - Tests, in `test/js/workerd/html-rewriter.test.js` (`on() registrations`), pin down the two things this storage has to get right. They pass before and after this change, since it is a refactor: - Many selectors registered on one rewriter, with two rejected `on()` calls in the middle, each still run the handlers they were registered with, on two transforms of the same rewriter. - `on()` called from inside a handler, often enough to reallocate the registry while lol-html is still calling the handlers registered before the transform started: the running transform is unaffected and the next one picks the additions up. With the `Box` removed from `ElementHandlerEntry` this test fails under ASAN with a heap-use-after-free (report in the details below), so the boxing is now covered rather than only commented. - Verified: - `bun bd test` on `test/js/workerd/html-rewriter.test.js` (165 tests, including the new ones), `html-rewriter-end-error.test.ts`, `html-rewriter-leak.test.ts`, `test/js/web/html/html-rewriter-doctype.test.ts` and the HTMLRewriter regression tests: all pass. - `cargo clippy -p bun_runtime --no-deps`: clean. - `cargo dylint --all -p bun_runtime` with this baseline: nothing over the baseline. The same command with the baseline line removed but the source change stashed reports exactly the one `parallel_vecs` finding for this file, so the removed line is the one this change fixes. - Regenerating the baseline with `MORDANT_BASELINE_WRITE=1` also drops two entries this PR does not touch (`always_unwrapped_option:src/install/PackageInstall.rs`, `narrowed_two_ways:src/runtime/node/node_crypto_binding.rs`); those findings were already fixed on main by other changes and are left for a separate cleanup. ### Background - `HTMLRewriter.on(selector, handlers)` parses the CSS selector with lol-html and wraps the JS handler object in an `ElementHandler` (the protected `element`/`comments`/`text` callbacks). Nothing is handed to lol-html at that point; registrations are collected in `LOLHTMLContext`, which is shared by the rewriter and every transform it starts, because `transform()` can run more than once. - `build_settings()` runs at transform time and turns each registration into a `(selector, ElementContentHandlers)` pair for lol-html. Its closures capture a `NonNull<ElementHandler>` pointing into the heap allocation owned by the `Box`, which is why the handler has to stay boxed even though clippy would normally suggest otherwise. An `on()` call after a transform has started (for example from inside a handler) pushes onto the same vec, which is what makes the reallocation case reachable from JS. - `mordant-baseline.toml` is the ratchet for the mordant lint pack run by the Rust lints workflow: it records the accepted number of findings per (lint, file), and CI reports anything above those counts. Removing the line here means a reintroduction of the pattern in this file would be reported. <details> <summary>ASAN report from the new test with the Box removed from ElementHandlerEntry</summary> ``` ERROR: AddressSanitizer: heap-use-after-free READ of size 8 #3 <ElementHandler as HandlerLike>::global src/runtime/api/html_rewriter.rs #4 handler_callback::<ElementHandler, Element, ...> src/runtime/api/html_rewriter.rs #5 ElementHandler::on_element src/runtime/api/html_rewriter.rs #6 build_settings::{closure#0} src/runtime/api/html_rewriter.rs #8 lol_html ContentHandlersDispatcher::handle_start_tag freed by thread T0 here: oven-sh#13 RawVec<ElementHandlerEntry>::grow_one oven-sh#15 Vec<ElementHandlerEntry>::push oven-sh#16 HTMLRewriter::on_ src/runtime/api/html_rewriter.rs ``` </details> <!-- robobun:evidence:begin --> --- **no test proof** · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/workerd/html-rewriter.test.js <!-- robobun:evidence:end --> --------- Co-authored-by: Alistair Smith <hi@alistair.sh>
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 hdrhistogram to version 0.11.10
Compare: HdrHistogram/HdrHistogram_c@be60a99...18c7a32
Auto-updated by this workflow