crypto: drop generateKeyPair ctx's KeyObject ref before invoking the callback - #36657
crypto: drop generateKeyPair ctx's KeyObject ref before invoking the callback#36657robobun wants to merge 4 commits into
Conversation
…callback KeyPairJobCtx::runFromJS held m_keyObj (a RefPtr<KeyObjectData>) across the user callback. The Rust ExternCtx Drop that deletes the ctx runs only after the callback returns, so when the callback calls process.exit() the ctx's ref is never released. lastChanceToFinalize destroys the JSPublicKeyObject/ JSPrivateKeyObject wrappers, but the stranded ctx ref keeps KeyObjectData (and its EVP_PKEY) alive past the leak check. With OPENSSL_memory_alloc hard-coded to mimalloc this was invisible to LSan; route the BoringSSL memory hooks through default_alloc so under cfg(bun_asan) they land on libc (matching the intent of dropping BORINGSSL_REQUIRE_MEMORY_HOOKS in the ASAN build config, which previously only took effect on Mach-O/COFF because ELF still resolved the weak hooks to these definitions), and drop the ctx's ref once the JS wrappers own the key so VM teardown frees it.
WalkthroughBoringSSL hooks now use default allocator APIs. Node crypto jobs release key references before JavaScript callback re-entry, including error paths. ASAN tests cover callback-triggered process exits for generated key objects and encrypted PEM outputs. ChangesMemory cleanup
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Status: reproduced locally (1480b / 35 allocs via CI build 87518: Note on the automated fail-before check: it stashes all of |
There was a problem hiding this comment.
Beyond the inline nit, I checked that default_alloc::{malloc,free,usable_size} are byte-identical to the previous direct mi_* calls under !cfg(bun_asan), so the BoringSSL hook change is a release no-op; and that lifting the #[cfg(any(debug_assertions, bun_asan))] gate on default_alloc::usable_size still compiles on Windows (the catch-all arm falls through to mi_usable_size).
Extended reasoning...
The inline nit (sibling SignJobCtx/DhJobCtx sharing the same hold-ref-across-callback pattern) is the only finding; it's an at-exit LSan report under a debug-only config with no CI coverage today, so it doesn't block. The two things I independently verified beyond it: (1) default_alloc::malloc/free/usable_size reduce to mi_malloc/mi_free/mi_usable_size when bun_asan is off, so release behavior of the OPENSSL memory hooks is unchanged; (2) removing the debug_assertions|bun_asan cfg gate on default_alloc::usable_size is safe on all check-all targets because the non-ASAN catch-all arm is mi_usable_size. Deferring to a human because the BoringSSL hook is process-global crypto/TLS allocator infrastructure and there's an open nit to address or acknowledge.
…llback Apply the same reset to the two sibling async crypto job contexts that hold RefPtr<KeyObjectData> across the user callback. Unlike generateKeyPair these take the key as an input that the caller already owns, and empirically do not trip LSan under BUN_DESTRUCT_VM_ON_EXIT=1 without the reset, but runTask has already produced the result and the ctx has no further use for the key ref.
|
Updated 11:05 PM PT - Aug 1st, 2026
❌ @robobun, your commit fffaa6b has 2 failures in
🧪 To try this PR locally: bunx bun-pr 36657That installs a local version of the PR into your bun-36657 --bun |
…EM output in the leak test
The only callers of the crate-root bun_alloc::usable_size were the two
BoringSSL hooks this branch rewired to default_alloc::usable_size; delete the
now-dead mimalloc-only wrapper so it cannot be picked up by accident.
The encrypted-PEM output path is the other generateKeyPair variant that
observably leaks without m_keyObj = {} before the callback (exportPrivate
returns a copied string, so the ctx ref going to zero is what frees the
EVP_PKEY). Cover it alongside the KeyObject-output case.
There was a problem hiding this comment.
Both prior findings are addressed in fffaa6b — the orphaned crate-root bun_alloc::usable_size is deleted (and named in the description), and the Scope section now states the intentional exclusions (GeneratePrimeJobCtx/CheckPrimeJobCtx/the KeyPairJobCtx passphrase) with the empirical no-leak table. No new issues found. Deferring only because the OPENSSL_memory_* hook rewire touches the allocator every TLS/crypto path shares — worth one human glance even though non-ASAN builds reduce to the same mi_* calls after inlining.
Checked: default_alloc::{malloc,free,usable_size} are all pub and the non-ASAN cfg arm falls through to mi_usable_size, so release behavior is unchanged; the RefPtr resets in KeyPairJobCtx/SignJobCtx/DhJobCtx all sit after the last read of the cleared field; the new tests follow harness conventions (bunEnv spread, test.concurrent, pipes drained via Promise.all, isASAN-gated).
Extended reasoning...
Overview
Six files: (1) src/boringssl/lib.rs rewires OPENSSL_memory_alloc/free/get_size from hard-coded mi_* to bun_alloc::default_alloc::*; (2) src/bun_alloc/lib.rs promotes default_alloc::usable_size from #[cfg(any(debug_assertions, bun_asan))] pub(crate) to unconditional pub, and deletes the now-orphaned crate-root usable_size wrapper; (3–5) three async crypto job contexts (KeyPairJobCtx, SignJobCtx, DhJobCtx) clear their RefPtr<KeyObjectData> fields before invoking the user callback; (6) two ASAN-gated leak regression tests spawning a child that calls process.exit(0) inside the generateKeyPair callback under BUN_DESTRUCT_VM_ON_EXIT=1 + detect_leaks=1.
What changed since the last review
Commit fffaa6b addressed both open findings: the dead crate-root usable_size is removed and named in the description, and the PR body's Scope section now enumerates the sibling contexts I flagged with an empirical leak/no-leak table and explicitly states why GeneratePrimeJobCtx/CheckPrimeJobCtx/the passphrase are left alone (not LSan-observable — the raw pointer lands on the runFromJS frame or in the ctx directly, so the conservative stack scan finds it). The second new test exercises the encrypted-PEM path, covering the passphrase-carrying m_privateKeyEncoding.
Security risks
None identified. No cryptographic logic, key-material handling, or validation changes — only earlier release of refcounts the ctx destructor would drop anyway, plus allocator routing that is a no-op in release builds (default_alloc on the non-ASAN arm is mi_malloc/mi_free/mi_usable_size). The rewire matches the convention documented in src/CLAUDE.md ("route through bun_alloc::default_alloc rather than mi_* directly").
Level of scrutiny
Medium-high. The ref resets are trivially correct (each field is last read before the reset). The allocator hook is the piece that warrants a human look: it sits under every OPENSSL_malloc in the process (all TLS handshakes, every node:crypto op). I verified the non-ASAN path is behavior-identical, but a maintainer should confirm they're comfortable with default_alloc::usable_size losing its #[cfg] guard and becoming a stable public entry point.
Other factors
All prior review threads (mine, the comment-cop bot's) are resolved. The fail-before caveat is honestly documented in the PR body. Test coverage is appropriate for an LSan-visibility fix — gated on isASAN, concurrent, 30s timeout justified by the symbolizer note.
|
Main now hits this exact leak: #36598 landed the same memory hook change (6b6fb1a), which made the stranded EVP_PKEY visible to LeakSanitizer, and test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts now fails on every Linux x64-asan run. The async-context-crypto-generateKeyPair.js fixture calls process.exit(0) inside the generateKeyPair callback, producing the signature documented above: direct leak of 24b in run (src/runtime/node/node_crypto_binding.rs:85:21) +34 more, 1480 bytes in 35 allocations. Examples: build 89023 and build 89031. Notes for landing this:
|
|
Superseded by #36986, which makes the job plumbing free the ctx before invoking the callback for all 11 job types instead of resetting individual ctx fields. The failing main test (AsyncLocalStorage-tracking on x64-asan) is covered there with the same fail-before signature. |
…llback (#36986) ## What `test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts` (the crypto-generateKeyPair fixture) fails on every Linux x64-asan run since #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; #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 #36598 (which made the leak visible); #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 -->
…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 -->
What
crypto.generateKeyPair's async completion (KeyPairJobCtx::runFromJS) keptm_keyObj(aRefPtr<KeyObjectData>) across the user callback. The RustExternCtxDrop thatdeletes the C++ ctx runs only afterthen()returns, so a callback that never returns (process.exit()) strands the ctx's ref.lastChanceToFinalizedestroys theJSPublicKeyObject/JSPrivateKeyObjectwrappers during VM teardown, but the stranded ctx ref keepsKeyObjectData(and theEVP_PKEYit owns) alive past the leak check.This also routes the BoringSSL memory hooks through
bun_alloc::default_allocinstead of hard-coding mimalloc. The ASAN build config already dropsBORINGSSL_REQUIRE_MEMORY_HOOKSso BoringSSL falls back to libc ("Off under ASAN so BoringSSL allocs stay on the intercepted libc heap"), but on ELF the weakOPENSSL_memory_alloc/free/get_sizesymbols still resolved to our definitions, which calledmi_mallocdirectly and kept everyOPENSSL_mallocallocation invisible to LeakSanitizer on Linux. Routing throughdefault_allocmakes them libc undercfg(bun_asan)everywhere. The crate-rootbun_alloc::usable_size(ami_usable_sizewrapper whose only callers were these hooks) is deleted.Reproduction
With the hook change alone:
from
test/js/node/async_hooks/async-context/async-context-crypto-generateKeyPair.js, which callsprocess.exit(0)inside the callback. First seen as the only[new]failure on the x64-asan lane of build 86653 for #36598, which applies the same hook change for its own LSan visibility.Cause
Ownership chain at the time the callback runs:
LSan scans the libc-backed
AnyTaskJobbox and the stack but can't follow through the FastMalloc blocks, so theEVP_PKEYis reported as a direct leak. The two JS key wrappers hold their ownRefPtr<KeyObjectData>but live in JSC GC cells (also not scanned as a root).Fix
runFromJSnow assignsm_keyObj = {}once the JS values have been produced (and on the two export-failure paths before the error callback). With the ctx's ref gone, either~VM -> lastChanceToFinalizedestroying the JS wrappers brings the refcount to zero (KeyObject output), or the refcount is already zero (string/buffer output), andEVP_PKEY_freeruns before LSan's atexit check.SignJobCtxandDhJobCtxget the same reset for theirRefPtr<KeyObjectData>fields (m_keyData,m_privateKey/m_publicKey).runTaskhas already produced the result and the ctx has no further use for the ref, so the reset is free.Scope
Tested each of the other async crypto job contexts under
BUN_DESTRUCT_VM_ON_EXIT=1+detect_leaks=1withprocess.exit(0)in the callback. With only theOPENSSL_memory_allochook applied andmain's C++:generateKeyPair(KeyObject output)generateKeyPair(PEM + passphrase output)signwith PEM-string keydiffieHellmangeneratePrimecheckPrimeOnly
generateKeyPairtrips LSan: itsEVP_PKEY*sits behind two FastMalloc indirections (ctx -> KeyObjectData* -> EVP_PKEY*) andrunFromJSnever loads the raw pointer onto the stack. The others hold theirOPENSSL_mallocpointer either directly in the ctx or load it onto therunFromJSframe (formemcpyinto the result buffer), so LSan's conservative stack scan finds it. TheSignJobCtx/DhJobCtxresets are kept for consistency;HkdfJobCtx::m_keyholds a symmetricVector<uint8_t>(FastMalloc, notOPENSSL_malloc) and is left alone, as areGeneratePrimeJobCtx/CheckPrimeJobCtxand theKeyPairJobCtxpassphrase, none of which are observable.Verification
New
isASAN-gated tests incrypto.key-objects.test.tsspawn a child withBUN_DESTRUCT_VM_ON_EXIT=1anddetect_leaks=1, generate an RSA key pair (once with KeyObject output, once with encrypted PEM output), and callprocess.exit(0)from the callback.m_keyObjreset: both cases fail with the 35-allocation LSan report abovetest-crypto-keygen-*parallel suite,test-crypto-sign-verify.js,test-crypto-dh-stateless.js, and allasync-context-crypto-*fixtures pass leak-cleanFail-before note
The automated fail-before check stashes all of
src/and rebuilds. With the hook reverted to mimalloc, LSan cannot see theEVP_PKEYallocation at all, so the new tests pass trivially onmain. The leak only becomes observable onceOPENSSL_memory_alloclands on libc, which is itself one of thesrc/changes in this diff; the fail-before/pass-after demonstration above (stashing onlyCryptoGenKeyPair.cpp) is the closest mechanical proof available.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/node/crypto/crypto.key-objects.test.ts