diff --git a/src/jsc/any_task_job.rs b/src/jsc/any_task_job.rs index 3dddfb2f96ba..f77d2eecd2d2 100644 --- a/src/jsc/any_task_job.rs +++ b/src/jsc/any_task_job.rs @@ -37,6 +37,7 @@ pub trait AnyTaskJobCtx: Sized { #[repr(C)] pub struct AnyTaskJob { run_from_js_erased: fn(*mut ()) -> JsResult<()>, + release_erased: fn(*mut ()), vm: bun_ptr::BackRef, task: WorkPoolTask, poll: KeepAlive, @@ -56,7 +57,24 @@ pub unsafe fn dispatch_erased(ptr: *mut ()) -> JsResult<()> { entry(ptr) } +/// Free a queued job at VM shutdown without running its completion; the ctx +/// `Drop` needs the still-live VM to release its JSC handles and resources. +/// +/// # Safety +/// `ptr` must be a live `*mut AnyTaskJob` from [`AnyTaskJob::create`], +/// popped from the event-loop queue (so it held exclusive ownership); frees it. +pub unsafe fn release_erased(ptr: *mut ()) { + // SAFETY: `AnyTaskJob` is `#[repr(C)]` with `release_erased` second; + // caller contract that `ptr` is such an allocation. + let entry = unsafe { *ptr.cast::().add(1) }; + entry(ptr) +} + const _: () = assert!(core::mem::offset_of!(AnyTaskJob<()>, run_from_js_erased) == 0); +const _: () = assert!( + core::mem::offset_of!(AnyTaskJob<()>, release_erased) + == core::mem::size_of:: JsResult<()>>() +); impl bun_event_loop::Taskable for AnyTaskJob { const TAG: bun_event_loop::TaskTag = bun_event_loop::task_tag::AnyTaskJob; @@ -82,6 +100,7 @@ impl AnyTaskJob { let vm = bun_ptr::BackRef::new(global.bun_vm()); let job = bun_core::heap::into_raw(Box::new(Self { run_from_js_erased: |p| Self::run_from_js(p.cast::()), + release_erased: |p| Self::release(p.cast::()), vm, task: WorkPoolTask { node: Default::default(), @@ -158,4 +177,11 @@ impl AnyTaskJob { } this.ctx.then(vm.global()) } + + /// [`release_erased`]'s monomorphic body. + fn release(this: *mut Self) { + // SAFETY: `this` was produced by `heap::into_raw` in `create`; the + // caller (the popped queue entry) held exclusive ownership. + drop(unsafe { bun_core::heap::take(this) }); + } } diff --git a/src/jsc/bindings/headers-handwritten.h b/src/jsc/bindings/headers-handwritten.h index 801e4b08022d..b379a3539483 100644 --- a/src/jsc/bindings/headers-handwritten.h +++ b/src/jsc/bindings/headers-handwritten.h @@ -402,9 +402,7 @@ extern "C" size_t Bun__encoding__byteLengthUTF16AsUTF8(const char16_t* ptr, size extern "C" JSC::EncodedJSValue Bun__encoding__constructFromLatin1(void*, const unsigned char* ptr, size_t len, Encoding encoding); extern "C" JSC::EncodedJSValue Bun__encoding__constructFromUTF16(void*, const char16_t* ptr, size_t len, Encoding encoding); -extern "C" void Bun__EventLoop__runCallback1(JSC::JSGlobalObject* global, JSC::EncodedJSValue callback, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue arg1); extern "C" void Bun__EventLoop__runCallback2(JSC::JSGlobalObject* global, JSC::EncodedJSValue callback, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue arg1, JSC::EncodedJSValue arg2); -extern "C" void Bun__EventLoop__runCallback3(JSC::JSGlobalObject* global, JSC::EncodedJSValue callback, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue arg1, JSC::EncodedJSValue arg2, JSC::EncodedJSValue arg3); /// @note throws a JS exception and returns false if a stack overflow occurs template diff --git a/src/jsc/bindings/node/crypto/CryptoDhJob.cpp b/src/jsc/bindings/node/crypto/CryptoDhJob.cpp index 8cb5b397b69b..1b9400e2c966 100644 --- a/src/jsc/bindings/node/crypto/CryptoDhJob.cpp +++ b/src/jsc/bindings/node/crypto/CryptoDhJob.cpp @@ -40,11 +40,11 @@ void DhJobCtx::runTask(JSGlobalObject* globalObject) m_result = ByteSource::allocated(dp.release()); } -extern "C" void Bun__DhJobCtx__runFromJS(DhJobCtx* ctx, JSGlobalObject* globalObject, EncodedJSValue callback) +extern "C" void Bun__DhJobCtx__runFromJS(DhJobCtx* ctx, JSGlobalObject* globalObject, JSCallbackArgs* out) { - ctx->runFromJS(globalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(globalObject); } -void DhJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback) +JSCallbackArgs DhJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject) { VM& vm = lexicalGlobalObject->vm(); ThrowScope scope = DECLARE_THROW_SCOPE(vm); @@ -52,18 +52,13 @@ void DhJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback) if (!m_result) { // Same message as the synchronous path so callers observe identical errors either way. JSObject* err = createError(lexicalGlobalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "diffieHellman operation failed"_s); - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(err)); - return; + RETURN_IF_EXCEPTION(scope, {}); + return { err }; } JSValue result = WebCore::createBuffer(lexicalGlobalObject, m_result.span()); - - Bun__EventLoop__runCallback2( - lexicalGlobalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(jsNull()), - JSValue::encode(result)); + RETURN_IF_EXCEPTION(scope, {}); + return { jsNull(), result }; } extern "C" DhJob* Bun__DhJob__create(JSGlobalObject* globalObject, DhJobCtx* ctx, EncodedJSValue callback); diff --git a/src/jsc/bindings/node/crypto/CryptoDhJob.h b/src/jsc/bindings/node/crypto/CryptoDhJob.h index 575597d5e5c2..4debc283b01d 100644 --- a/src/jsc/bindings/node/crypto/CryptoDhJob.h +++ b/src/jsc/bindings/node/crypto/CryptoDhJob.h @@ -1,6 +1,7 @@ #pragma once #include "root.h" +#include "JSCallbackArgs.h" #include "KeyObject.h" #include "CryptoUtil.h" @@ -30,7 +31,7 @@ struct DhJobCtx { static std::optional fromJS(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSObject* options); void runTask(JSC::JSGlobalObject*); - void runFromJS(JSC::JSGlobalObject*, JSC::JSValue callback); + JSCallbackArgs runFromJS(JSC::JSGlobalObject*); void deinit(); RefPtr m_privateKey; diff --git a/src/jsc/bindings/node/crypto/CryptoGenDhKeyPair.cpp b/src/jsc/bindings/node/crypto/CryptoGenDhKeyPair.cpp index 86ab37651815..e31d9101704b 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenDhKeyPair.cpp +++ b/src/jsc/bindings/node/crypto/CryptoGenDhKeyPair.cpp @@ -25,9 +25,9 @@ extern "C" void Bun__DhKeyPairJobCtx__runTask(DhKeyPairJobCtx* ctx, JSGlobalObje ctx->runTask(globalObject, keyCtx); } -extern "C" void Bun__DhKeyPairJobCtx__runFromJS(DhKeyPairJobCtx* ctx, JSGlobalObject* globalObject, EncodedJSValue callback) +extern "C" void Bun__DhKeyPairJobCtx__runFromJS(DhKeyPairJobCtx* ctx, JSGlobalObject* globalObject, JSCallbackArgs* out) { - ctx->runFromJS(globalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(globalObject); } extern "C" DhKeyPairJob* Bun__DhKeyPairJob__create(JSGlobalObject* globalObject, DhKeyPairJobCtx* ctx, EncodedJSValue callback); diff --git a/src/jsc/bindings/node/crypto/CryptoGenDsaKeyPair.cpp b/src/jsc/bindings/node/crypto/CryptoGenDsaKeyPair.cpp index 23b38a6f4aec..e992b05eb0f7 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenDsaKeyPair.cpp +++ b/src/jsc/bindings/node/crypto/CryptoGenDsaKeyPair.cpp @@ -24,9 +24,9 @@ extern "C" void Bun__DsaKeyPairJobCtx__runTask(DsaKeyPairJobCtx* ctx, JSGlobalOb ctx->runTask(globalObject, keyCtx); } -extern "C" void Bun__DsaKeyPairJobCtx__runFromJS(DsaKeyPairJobCtx* ctx, JSGlobalObject* globalObject, EncodedJSValue callback) +extern "C" void Bun__DsaKeyPairJobCtx__runFromJS(DsaKeyPairJobCtx* ctx, JSGlobalObject* globalObject, JSCallbackArgs* out) { - ctx->runFromJS(globalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(globalObject); } extern "C" DsaKeyPairJob* Bun__DsaKeyPairJob__create(JSGlobalObject* globalObject, DsaKeyPairJobCtx* ctx, EncodedJSValue callback); diff --git a/src/jsc/bindings/node/crypto/CryptoGenEcKeyPair.cpp b/src/jsc/bindings/node/crypto/CryptoGenEcKeyPair.cpp index 17614c93ee03..4df1f1927b41 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenEcKeyPair.cpp +++ b/src/jsc/bindings/node/crypto/CryptoGenEcKeyPair.cpp @@ -25,9 +25,9 @@ extern "C" void Bun__EcKeyPairJobCtx__runTask(EcKeyPairJobCtx* ctx, JSGlobalObje ctx->runTask(globalObject, keyCtx); } -extern "C" void Bun__EcKeyPairJobCtx__runFromJS(EcKeyPairJobCtx* ctx, JSGlobalObject* globalObject, EncodedJSValue callback) +extern "C" void Bun__EcKeyPairJobCtx__runFromJS(EcKeyPairJobCtx* ctx, JSGlobalObject* globalObject, JSCallbackArgs* out) { - ctx->runFromJS(globalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(globalObject); } extern "C" EcKeyPairJob* Bun__EcKeyPairJob__create(JSGlobalObject* globalObject, EcKeyPairJobCtx* ctx, EncodedJSValue callback); diff --git a/src/jsc/bindings/node/crypto/CryptoGenKeyPair.cpp b/src/jsc/bindings/node/crypto/CryptoGenKeyPair.cpp index 9f3c0233f77b..943f77e4b121 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenKeyPair.cpp +++ b/src/jsc/bindings/node/crypto/CryptoGenKeyPair.cpp @@ -29,44 +29,33 @@ void KeyPairJobCtx::runTask(JSGlobalObject* globalObject, ncrypto::EVPKeyCtxPoin m_keyObj = KeyObject::create(CryptoKeyType::Private, WTF::move(key)); } -void KeyPairJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback) +JSCallbackArgs KeyPairJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject) { VM& vm = lexicalGlobalObject->vm(); ThrowScope scope = DECLARE_THROW_SCOPE(vm); - auto exceptionCallback = [lexicalGlobalObject, callback](JSValue exceptionValue) { - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(exceptionValue)); - }; - if (!m_keyObj.data()) { JSValue err = createCryptoError(lexicalGlobalObject, scope, m_opensslError, "key generation failed"_s); - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(err)); - return; + RETURN_IF_EXCEPTION(scope, {}); + return { err }; } JSValue publicKeyValue = m_keyObj.exportPublic(lexicalGlobalObject, scope, m_publicKeyEncoding); if (scope.exception()) [[unlikely]] { - JSValue exceptionValue = scope.exception(); + // The thrown Error, not the Exception cell (node parity). + JSValue exceptionValue = scope.exception()->value(); (void)scope.tryClearException(); - exceptionCallback(exceptionValue); - return; + return { exceptionValue }; } JSValue privateKeyValue = m_keyObj.exportPrivate(lexicalGlobalObject, scope, m_privateKeyEncoding); if (scope.exception()) [[unlikely]] { - JSValue exceptionValue = scope.exception(); + JSValue exceptionValue = scope.exception()->value(); (void)scope.tryClearException(); - exceptionCallback(exceptionValue); - return; + return { exceptionValue }; } - Bun__EventLoop__runCallback3( - lexicalGlobalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(jsNull()), - JSValue::encode(publicKeyValue), - JSValue::encode(privateKeyValue)); + return { jsNull(), publicKeyValue, privateKeyValue }; } KeyEncodingConfig parseKeyEncodingConfig(JSGlobalObject* globalObject, ThrowScope& scope, JSValue keyTypeValue, JSValue optionsValue) diff --git a/src/jsc/bindings/node/crypto/CryptoGenKeyPair.h b/src/jsc/bindings/node/crypto/CryptoGenKeyPair.h index 3bc4c5a244ee..42fd3f3289f3 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenKeyPair.h +++ b/src/jsc/bindings/node/crypto/CryptoGenKeyPair.h @@ -1,6 +1,7 @@ #pragma once #include "root.h" +#include "JSCallbackArgs.h" #include "ncrypto.h" #include "KeyObject.h" @@ -23,7 +24,7 @@ struct KeyPairJobCtx { } void runTask(JSC::JSGlobalObject* globalObject, ncrypto::EVPKeyCtxPointer& ctx); - void runFromJS(JSC::JSGlobalObject* globalObject, JSC::JSValue callback); + JSCallbackArgs runFromJS(JSC::JSGlobalObject* globalObject); void deinit(); int err() const { return m_opensslError; }; diff --git a/src/jsc/bindings/node/crypto/CryptoGenNidKeyPair.cpp b/src/jsc/bindings/node/crypto/CryptoGenNidKeyPair.cpp index e6ec57136c2c..405413a109c1 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenNidKeyPair.cpp +++ b/src/jsc/bindings/node/crypto/CryptoGenNidKeyPair.cpp @@ -25,9 +25,9 @@ extern "C" void Bun__NidKeyPairJobCtx__runTask(NidKeyPairJobCtx* ctx, JSGlobalOb ctx->runTask(globalObject, keyCtx); } -extern "C" void Bun__NidKeyPairJobCtx__runFromJS(NidKeyPairJobCtx* ctx, JSGlobalObject* globalObject, EncodedJSValue callback) +extern "C" void Bun__NidKeyPairJobCtx__runFromJS(NidKeyPairJobCtx* ctx, JSGlobalObject* globalObject, JSCallbackArgs* out) { - ctx->runFromJS(globalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(globalObject); } extern "C" NidKeyPairJob* Bun__NidKeyPairJob__create(JSGlobalObject* globalObject, NidKeyPairJobCtx* ctx, EncodedJSValue callback); diff --git a/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp b/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp index 648374da83b1..b9d16ac67fec 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp +++ b/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp @@ -26,9 +26,9 @@ extern "C" void Bun__RsaKeyPairJobCtx__runTask(RsaKeyPairJobCtx* ctx, JSGlobalOb ctx->runTask(globalObject, keyCtx); } -extern "C" void Bun__RsaKeyPairJobCtx__runFromJS(RsaKeyPairJobCtx* ctx, JSGlobalObject* globalObject, EncodedJSValue callback) +extern "C" void Bun__RsaKeyPairJobCtx__runFromJS(RsaKeyPairJobCtx* ctx, JSGlobalObject* globalObject, JSCallbackArgs* out) { - ctx->runFromJS(globalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(globalObject); } extern "C" RsaKeyPairJob* Bun__RsaKeyPairJob__create(JSGlobalObject* globalObject, RsaKeyPairJobCtx* ctx, EncodedJSValue callback); diff --git a/src/jsc/bindings/node/crypto/CryptoHkdf.cpp b/src/jsc/bindings/node/crypto/CryptoHkdf.cpp index ef87999cdd4f..c05d3e33a580 100644 --- a/src/jsc/bindings/node/crypto/CryptoHkdf.cpp +++ b/src/jsc/bindings/node/crypto/CryptoHkdf.cpp @@ -67,19 +67,19 @@ void HkdfJobCtx::runTask(JSGlobalObject* lexicalGlobalObject) m_result = ByteSource::allocated(dp.release()); } -extern "C" void Bun__HkdfJobCtx__runFromJS(HkdfJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, EncodedJSValue callback) +extern "C" void Bun__HkdfJobCtx__runFromJS(HkdfJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, JSCallbackArgs* out) { - ctx->runFromJS(lexicalGlobalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(lexicalGlobalObject); } -void HkdfJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback) +JSCallbackArgs HkdfJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject) { auto& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); if (!m_result) { JSObject* err = createError(lexicalGlobalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "hkdf operation failed"_s); - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(err)); - return; + RETURN_IF_EXCEPTION(scope, {}); + return { err }; } auto& result = m_result.value(); @@ -88,17 +88,15 @@ void HkdfJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback RefPtr buf = ArrayBuffer::tryCreateUninitialized(result.size(), 1); if (!buf) { JSObject* err = createOutOfMemoryError(lexicalGlobalObject); - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(err)); - return; + RETURN_IF_EXCEPTION(scope, {}); + return { err }; } memcpy(buf->data(), result.data(), result.size()); - Bun__EventLoop__runCallback2(lexicalGlobalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(jsNull()), - JSValue::encode(JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), buf.releaseNonNull()))); + JSValue resultBuffer = JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), buf.releaseNonNull()); + RETURN_IF_EXCEPTION(scope, {}); + return { jsNull(), resultBuffer }; } extern "C" void Bun__HkdfJobCtx__deinit(HkdfJobCtx* ctx) diff --git a/src/jsc/bindings/node/crypto/CryptoHkdf.h b/src/jsc/bindings/node/crypto/CryptoHkdf.h index 8689cea6716b..06d0d4a86365 100644 --- a/src/jsc/bindings/node/crypto/CryptoHkdf.h +++ b/src/jsc/bindings/node/crypto/CryptoHkdf.h @@ -1,6 +1,7 @@ #pragma once #include "root.h" +#include "JSCallbackArgs.h" #include "helpers.h" #include "ncrypto.h" #include "CryptoUtil.h" @@ -25,7 +26,7 @@ struct HkdfJobCtx { static std::optional fromJS(JSC::JSGlobalObject*, JSC::CallFrame*, JSC::ThrowScope&, Mode); void runTask(JSC::JSGlobalObject*); - void runFromJS(JSC::JSGlobalObject*, JSC::JSValue callback); + JSCallbackArgs runFromJS(JSC::JSGlobalObject*); void deinit(); ncrypto::Digest m_digest; diff --git a/src/jsc/bindings/node/crypto/CryptoKeygen.cpp b/src/jsc/bindings/node/crypto/CryptoKeygen.cpp index d3f73952e173..d44bd588db5a 100644 --- a/src/jsc/bindings/node/crypto/CryptoKeygen.cpp +++ b/src/jsc/bindings/node/crypto/CryptoKeygen.cpp @@ -35,11 +35,11 @@ void SecretKeyJobCtx::runTask(JSGlobalObject* lexicalGlobalObject) m_result = WTF::move(key); } -extern "C" void Bun__SecretKeyJobCtx__runFromJS(SecretKeyJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, JSC::JSValue callback) +extern "C" void Bun__SecretKeyJobCtx__runFromJS(SecretKeyJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, JSCallbackArgs* out) { - ctx->runFromJS(lexicalGlobalObject, callback); + *out = ctx->runFromJS(lexicalGlobalObject); } -void SecretKeyJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSC::JSValue callback) +JSCallbackArgs SecretKeyJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject) { VM& vm = lexicalGlobalObject->vm(); ThrowScope scope = DECLARE_THROW_SCOPE(vm); @@ -47,20 +47,18 @@ void SecretKeyJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSC::JSValu if (!m_result) { JSObject* err = createError(lexicalGlobalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "key generation failed"_s); - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(err)); - return; + RETURN_IF_EXCEPTION(scope, {}); + return { err }; } KeyObject keyObject = KeyObject::create(WTF::move(*m_result)); Structure* structure = globalObject->m_JSSecretKeyObjectClassStructure.get(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); JSSecretKeyObject* secretKey = JSSecretKeyObject::create(vm, structure, lexicalGlobalObject, WTF::move(keyObject)); + RETURN_IF_EXCEPTION(scope, {}); - Bun__EventLoop__runCallback2(lexicalGlobalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(jsNull()), - JSValue::encode(secretKey)); + return { jsNull(), secretKey }; } extern "C" void Bun__SecretKeyJobCtx__deinit(SecretKeyJobCtx* ctx) diff --git a/src/jsc/bindings/node/crypto/CryptoKeygen.h b/src/jsc/bindings/node/crypto/CryptoKeygen.h index e5ab150fcf5f..f952fbeb838a 100644 --- a/src/jsc/bindings/node/crypto/CryptoKeygen.h +++ b/src/jsc/bindings/node/crypto/CryptoKeygen.h @@ -1,6 +1,7 @@ #pragma once #include "root.h" +#include "JSCallbackArgs.h" #include "ncrypto.h" namespace Bun { @@ -11,7 +12,7 @@ struct SecretKeyJobCtx { ~SecretKeyJobCtx() = default; void runTask(JSC::JSGlobalObject* lexicalGlobalObject); - void runFromJS(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue callback); + JSCallbackArgs runFromJS(JSC::JSGlobalObject* lexicalGlobalObject); void deinit(); static std::optional fromJS(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSValue typeValue, JSC::JSValue optionsValue); diff --git a/src/jsc/bindings/node/crypto/CryptoPrimes.cpp b/src/jsc/bindings/node/crypto/CryptoPrimes.cpp index 2dda73ce7a30..f55096657616 100644 --- a/src/jsc/bindings/node/crypto/CryptoPrimes.cpp +++ b/src/jsc/bindings/node/crypto/CryptoPrimes.cpp @@ -33,13 +33,13 @@ void CheckPrimeJobCtx::runTask(JSGlobalObject* lexicalGlobalObject) m_result = res != 0; } -extern "C" void Bun__CheckPrimeJobCtx__runFromJS(CheckPrimeJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, EncodedJSValue callback) +extern "C" void Bun__CheckPrimeJobCtx__runFromJS(CheckPrimeJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, JSCallbackArgs* out) { - ctx->runFromJS(lexicalGlobalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(lexicalGlobalObject); } -void CheckPrimeJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback) +JSCallbackArgs CheckPrimeJobCtx::runFromJS(JSGlobalObject*) { - Bun__EventLoop__runCallback2(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(jsUndefined()), JSValue::encode(jsBoolean(m_result))); + return { jsUndefined(), jsBoolean(m_result) }; } extern "C" void Bun__CheckPrimeJobCtx__deinit(CheckPrimeJobCtx* ctx) @@ -196,11 +196,11 @@ void GeneratePrimeJobCtx::runTask(JSGlobalObject* lexicalGlobalObject) }); } -extern "C" void Bun__GeneratePrimeJobCtx__runFromJS(GeneratePrimeJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, EncodedJSValue callback) +extern "C" void Bun__GeneratePrimeJobCtx__runFromJS(GeneratePrimeJobCtx* ctx, JSGlobalObject* lexicalGlobalObject, JSCallbackArgs* out) { - ctx->runFromJS(lexicalGlobalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(lexicalGlobalObject); } -void GeneratePrimeJobCtx::runFromJS(JSGlobalObject* globalObject, JSValue callback) +JSCallbackArgs GeneratePrimeJobCtx::runFromJS(JSGlobalObject* globalObject) { auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -208,23 +208,13 @@ void GeneratePrimeJobCtx::runFromJS(JSGlobalObject* globalObject, JSValue callba JSValue result = GeneratePrimeJob::result(globalObject, scope, m_prime, m_bigint); EXCEPTION_ASSERT(result.isEmpty() == !!scope.exception()); if (scope.exception()) [[unlikely]] { - auto* err = scope.exception(); + // The thrown Error, not the Exception cell (node parity). + JSValue err = scope.exception()->value(); (void)scope.tryClearException(); - Bun__EventLoop__runCallback1( - globalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(err)); - return; + return { err }; } - Bun__EventLoop__runCallback2( - globalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(jsUndefined()), - JSValue::encode(result)); - return; + return { jsUndefined(), result }; } extern "C" void Bun__GeneratePrimeJobCtx__deinit(GeneratePrimeJobCtx* ctx) diff --git a/src/jsc/bindings/node/crypto/CryptoPrimes.h b/src/jsc/bindings/node/crypto/CryptoPrimes.h index 1f8f36fd605b..fa180abd94f4 100644 --- a/src/jsc/bindings/node/crypto/CryptoPrimes.h +++ b/src/jsc/bindings/node/crypto/CryptoPrimes.h @@ -1,6 +1,7 @@ #pragma once #include "root.h" +#include "JSCallbackArgs.h" #include "helpers.h" #include "ncrypto.h" @@ -11,7 +12,7 @@ struct CheckPrimeJobCtx { ~CheckPrimeJobCtx(); void runTask(JSC::JSGlobalObject* lexicalGlobalObject); - void runFromJS(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue callback); + JSCallbackArgs runFromJS(JSC::JSGlobalObject* lexicalGlobalObject); void deinit(); int32_t m_checks; @@ -35,7 +36,7 @@ struct GeneratePrimeJobCtx { ~GeneratePrimeJobCtx(); void runTask(JSC::JSGlobalObject* lexicalGlobalObject); - void runFromJS(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue callback); + JSCallbackArgs runFromJS(JSC::JSGlobalObject* lexicalGlobalObject); void deinit(); int32_t m_size; diff --git a/src/jsc/bindings/node/crypto/CryptoSignJob.cpp b/src/jsc/bindings/node/crypto/CryptoSignJob.cpp index 2d726dda6f5f..d9e8aa5259cf 100644 --- a/src/jsc/bindings/node/crypto/CryptoSignJob.cpp +++ b/src/jsc/bindings/node/crypto/CryptoSignJob.cpp @@ -215,11 +215,11 @@ void SignJobCtx::runTask(JSGlobalObject* globalObject) } } -extern "C" void Bun__SignJobCtx__runFromJS(SignJobCtx* ctx, JSGlobalObject* globalObject, EncodedJSValue callback) +extern "C" void Bun__SignJobCtx__runFromJS(SignJobCtx* ctx, JSGlobalObject* globalObject, JSCallbackArgs* out) { - ctx->runFromJS(globalObject, JSValue::decode(callback)); + *out = ctx->runFromJS(globalObject); } -void SignJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback) +JSCallbackArgs SignJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject) { auto& vm = lexicalGlobalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -230,8 +230,8 @@ void SignJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback JSValue err = m_unsupportedContext ? createError(lexicalGlobalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Context parameter is unsupported"_s) : createCryptoError(lexicalGlobalObject, scope, m_opensslError, "sign operation failed"_s); - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(err)); - return; + RETURN_IF_EXCEPTION(scope, {}); + return { err }; } auto* globalObject = defaultGlobalObject(lexicalGlobalObject); @@ -239,35 +239,23 @@ void SignJobCtx::runFromJS(JSGlobalObject* lexicalGlobalObject, JSValue callback auto sigBuf = ArrayBuffer::createUninitialized(m_signResult->size(), 1); memcpy(sigBuf->data(), m_signResult->data(), m_signResult->size()); auto* signature = JSUint8Array::create(lexicalGlobalObject, globalObject->JSBufferSubclassStructure(), WTF::move(sigBuf), 0, m_signResult->size()); - RETURN_IF_EXCEPTION(scope, ); - - Bun__EventLoop__runCallback2( - lexicalGlobalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(jsNull()), - JSValue::encode(signature)); + RETURN_IF_EXCEPTION(scope, {}); - break; + return { jsNull(), signature }; } case Mode::Verify: { if (!m_verifyResult) { JSValue err = m_unsupportedContext ? createError(lexicalGlobalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Context parameter is unsupported"_s) : createCryptoError(lexicalGlobalObject, scope, m_opensslError, "verify operation failed"_s); - Bun__EventLoop__runCallback1(lexicalGlobalObject, JSValue::encode(callback), JSValue::encode(jsUndefined()), JSValue::encode(err)); - return; + RETURN_IF_EXCEPTION(scope, {}); + return { err }; } - Bun__EventLoop__runCallback2( - lexicalGlobalObject, - JSValue::encode(callback), - JSValue::encode(jsUndefined()), - JSValue::encode(jsNull()), - JSValue::encode(jsBoolean(*m_verifyResult))); - break; + return { jsNull(), jsBoolean(*m_verifyResult) }; } } + return {}; } extern "C" SignJob* Bun__SignJob__create(JSGlobalObject* globalObject, SignJobCtx* ctx, EncodedJSValue callback); diff --git a/src/jsc/bindings/node/crypto/CryptoSignJob.h b/src/jsc/bindings/node/crypto/CryptoSignJob.h index 18743ccbea38..694ea27b19a0 100644 --- a/src/jsc/bindings/node/crypto/CryptoSignJob.h +++ b/src/jsc/bindings/node/crypto/CryptoSignJob.h @@ -1,6 +1,7 @@ #pragma once #include "root.h" +#include "JSCallbackArgs.h" #include "CryptoUtil.h" #include "KeyObject.h" @@ -49,7 +50,7 @@ struct SignJobCtx { JSValue algorithmValue, JSValue dataValue, JSValue keyValue, JSValue signatureValue, JSValue callbackValue); void runTask(JSC::JSGlobalObject*); - void runFromJS(JSC::JSGlobalObject*, JSC::JSValue callback); + JSCallbackArgs runFromJS(JSC::JSGlobalObject*); void deinit(); Mode m_mode; diff --git a/src/jsc/bindings/node/crypto/JSCallbackArgs.h b/src/jsc/bindings/node/crypto/JSCallbackArgs.h new file mode 100644 index 000000000000..5cdcf8678c9f --- /dev/null +++ b/src/jsc/bindings/node/crypto/JSCallbackArgs.h @@ -0,0 +1,39 @@ +#pragma once + +#include "root.h" + +namespace Bun { + +// The arguments an async crypto job's completion callback will be invoked +// with, returned by value from the ctx's JS-thread half (`runFromJS`). The +// native side only builds this value; the job plumbing in +// node_crypto_binding.rs frees the ctx and then calls the JS callback with it +// (mirrored there as `JsCallbackArgs`). The default-constructed value (no +// arguments) is the discard returned alongside a pending exception. +struct JSCallbackArgs { + JSCallbackArgs() = default; + JSCallbackArgs(JSC::JSValue arg0) + : m_argv { JSC::JSValue::encode(arg0) } + , m_argc(1) + { + } + JSCallbackArgs(JSC::JSValue arg0, JSC::JSValue arg1) + : m_argv { JSC::JSValue::encode(arg0), JSC::JSValue::encode(arg1) } + , m_argc(2) + { + } + JSCallbackArgs(JSC::JSValue arg0, JSC::JSValue arg1, JSC::JSValue arg2) + : m_argv { JSC::JSValue::encode(arg0), JSC::JSValue::encode(arg1), JSC::JSValue::encode(arg2) } + , m_argc(3) + { + } + +private: + JSC::EncodedJSValue m_argv[3] = { 0, 0, 0 }; + // Read on the Rust side only (node_crypto_binding.rs). + [[maybe_unused]] uint32_t m_argc = 0; +}; + +static_assert(std::is_trivially_copyable_v, "copied through an extern \"C\" out-pointer"); + +} // namespace Bun diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index d12b7963a69f..29fc3082309f 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -743,9 +743,9 @@ impl EventLoop { /// /// Tags `__bun_release_task_at_shutdown` doesn't claim are likewise /// re-queued so they remain reachable from the static-rooted VM box (the - /// pre-`532a5411961b` state). Consuming them silently here unhooked that - /// root and surfaced the boxes as direct leaks (e.g. `AnyTaskJob<_>`); the - /// definer can't safely dispatch every erased callback at shutdown. + /// pre-`532a5411961b` state). Consuming them without freeing unhooked that + /// root and surfaced the boxes as direct leaks; the definer can't safely + /// dispatch every erased callback at shutdown. pub fn release_queued_tasks_for_shutdown(&mut self) { self.drop_concurrent_cpp_tasks(); let mut requeue: Vec = Vec::new(); @@ -1223,19 +1223,6 @@ extern "C" fn noop_forever_timer(_: *mut uws::Timer) { // do nothing } -// HOST_EXPORT(Bun__EventLoop__runCallback1, c) -pub fn event_loop_run_callback1( - global: &JSGlobalObject, - callback: JSValue, - this_value: JSValue, - arg0: JSValue, -) { - global - .bun_vm() - .event_loop_mut() - .run_callback(callback, global, this_value, &[arg0]); -} - // HOST_EXPORT(Bun__EventLoop__runCallback2, c) pub fn event_loop_run_callback2( global: &JSGlobalObject, @@ -1250,23 +1237,6 @@ pub fn event_loop_run_callback2( .run_callback(callback, global, this_value, &[arg0, arg1]); } -// HOST_EXPORT(Bun__EventLoop__runCallback3, c) -pub fn event_loop_run_callback3( - global: &JSGlobalObject, - callback: JSValue, - this_value: JSValue, - arg0: JSValue, - arg1: JSValue, - arg2: JSValue, -) { - global.bun_vm().event_loop_mut().run_callback( - callback, - global, - this_value, - &[arg0, arg1, arg2], - ); -} - // HOST_EXPORT(Bun__EventLoop__enter, c) pub fn event_loop_enter(global: &JSGlobalObject) { global.bun_vm().event_loop_mut().enter(); diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index 7a9f2bb1618d..ddaae8077962 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -1355,6 +1355,14 @@ fn __bun_release_task_at_shutdown(task: bun_event_loop::Task) -> bool { unsafe { Bun__deleteEventLoopTask(task.ptr.cast::()) }; true } + // Queue presence means the work-pool phase finished and the queue + // owned the job; parking it would strand the ctx's native resources. + task_tag::AnyTaskJob => { + // SAFETY: every queued AnyTaskJob payload is the live heap job + // created by `AnyTaskJob::create`; we own it once popped. + unsafe { bun_jsc::any_task_job::release_erased(task.ptr) }; + true + } // Re-queued by the caller; the box stays reachable from the // static-rooted VM queue, because running these callbacks // is not generally safe at shutdown (e.g. `AsyncModule::on_done`, diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index b52e71deb6c3..3f3bc7672915 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -54,6 +54,27 @@ impl JSValueCryptoExt for JSValue { // ExternCryptoJob — token-pastes C symbol names (`Bun__Ctx__runTask` // etc.), so a `macro_rules!` is the right shape. // ─────────────────────────────────────────────────────────────────────────── + +/// Completion-callback arguments produced by a job ctx's JS-thread half +/// (`runFromJS`). Layout mirrors `Bun::JSCallbackArgs` (JSCallbackArgs.h), +/// which fills it through the extern "C" out-pointer. +#[repr(C)] +struct JsCallbackArgs { + argv: [JSValue; 3], + argc: u32, +} + +impl JsCallbackArgs { + const EMPTY: Self = Self { + argv: [JSValue::UNDEFINED; 3], + argc: 0, + }; + + fn as_slice(&self) -> &[JSValue] { + &self.argv[..(self.argc as usize).min(self.argv.len())] + } +} + macro_rules! extern_crypto_job { ($Name:ident, $name_str:literal) => { pub mod $Name { @@ -66,20 +87,37 @@ macro_rules! extern_crypto_job { // to a non-null pointer and discharges the validity proof at the // type level. `global` in `runTask` is forwarded raw (the trait // hands us `*mut`; C++ never reads through it off-thread). + // + // `runFromJS` (the JS-thread half; `runTask` is the work-pool + // half) returns the completion callback's arguments by value. It + // never sees the callback, so it cannot run user JS; `then` frees + // the ctx and then invokes. unsafe extern "C" { #[link_name = concat!("Bun__", $name_str, "Ctx__runTask")] safe fn ctx_run_task(ctx: &Ctx, global: *mut JSGlobalObject); #[link_name = concat!("Bun__", $name_str, "Ctx__runFromJS")] - safe fn ctx_run_from_js(ctx: &Ctx, global: &JSGlobalObject, callback: JSValue); + safe fn ctx_run_from_js( + ctx: &Ctx, + global: &JSGlobalObject, + out: &mut JsCallbackArgs, + ); #[link_name = concat!("Bun__", $name_str, "Ctx__deinit")] safe fn ctx_deinit(ctx: &Ctx); } pub(crate) struct ExternCtx { + // Null once `then` has freed it. ctx: *mut Ctx, callback: StrongOptional, } + impl ExternCtx { + fn deinit_ctx(&mut self) { + ctx_deinit(Ctx::opaque_ref(self.ctx)); + self.ctx = core::ptr::null_mut(); + } + } + impl AnyTaskJobCtx for ExternCtx { fn run(&mut self, global: *mut JSGlobalObject) { ctx_run_task(Ctx::opaque_ref(self.ctx), global); @@ -88,11 +126,24 @@ macro_rules! extern_crypto_job { let Some(callback) = self.callback.try_swap() else { return Ok(()); }; - let ctx = Ctx::opaque_ref(self.ctx); - if let Err(err) = jsc::from_js_host_call_generic(global, || { - ctx_run_from_js(ctx, global, callback); - }) { - global.report_active_exception_as_unhandled(err); + let mut args = JsCallbackArgs::EMPTY; + let produced = jsc::from_js_host_call_generic(global, || { + ctx_run_from_js(Ctx::opaque_ref(self.ctx), global, &mut args); + }); + // Free the ctx before user JS (the callback, or an + // uncaughtException handler) — user code may never return + // (`process.exit()`). + self.deinit_ctx(); + match produced { + Ok(()) => { + global.bun_vm().event_loop_mut().run_callback( + callback, + global, + JSValue::UNDEFINED, + args.as_slice(), + ); + } + Err(err) => global.report_active_exception_as_unhandled(err), } Ok(()) } @@ -100,7 +151,11 @@ macro_rules! extern_crypto_job { impl Drop for ExternCtx { fn drop(&mut self) { - ctx_deinit(Ctx::opaque_ref(self.ctx)); + // Non-null when the job dies without completing (shutdown + // early-out, `init` failure, missing callback). + if !self.ctx.is_null() { + self.deinit_ctx(); + } self.callback.deinit(); } } diff --git a/test/js/node/crypto/crypto.key-objects.test.ts b/test/js/node/crypto/crypto.key-objects.test.ts index 5f433d9cf161..58017268821c 100644 --- a/test/js/node/crypto/crypto.key-objects.test.ts +++ b/test/js/node/crypto/crypto.key-objects.test.ts @@ -23,7 +23,7 @@ import { verify, } from "crypto"; import fs from "fs"; -import { isWindows } from "harness"; +import { bunEnv, bunExe, isASAN, isWindows } from "harness"; import { createContext, runInContext, runInThisContext, Script } from "node:vm"; import path from "path"; @@ -1800,3 +1800,91 @@ test("ECDSA should work", async () => { function randomProp() { return "prop" + crypto.randomUUID().replace(/-/g, ""); } + +test("generateKeyPair passes the thrown Error to the callback when key export fails", async () => { + const { promise, resolve } = Promise.withResolvers(); + // P-224 keygen succeeds, JWK export does not, driving the caught-exception + // branch of the async completion. Node surfaces the Error object itself. + generateKeyPair( + "ec", + { + namedCurve: "secp224r1", + publicKeyEncoding: { format: "jwk" }, + privateKeyEncoding: { format: "jwk" }, + } as any, + err => resolve(err as any), + ); + const err = await promise; + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe("ERR_CRYPTO_JWK_UNSUPPORTED_CURVE"); + expect(err.message).toContain("Unsupported JWK EC curve"); +}); + +// The async crypto jobs (generateKeyPair, sign, diffieHellman, hkdf, ...) run +// on the work pool and complete on the JS thread. The native job ctx must be +// freed before the JS callback is invoked: a callback that never returns +// (process.exit()) would otherwise strand everything the ctx still owns (the +// generated EVP_PKEY, key refs, BIGNUMs). These children run with leak +// checking on, so a stranded OpenSSL allocation fails the child with a +// LeakSanitizer report. +describe.skipIf(!isASAN)("async crypto jobs: process.exit() in the callback leaks nothing", () => { + const lsanEnv = { + ...bunEnv, + BUN_DESTRUCT_VM_ON_EXIT: "1", + ASAN_OPTIONS: [bunEnv.ASAN_OPTIONS, "detect_leaks=1"].filter(Boolean).join(":"), + LSAN_OPTIONS: `print_suppressions=0:suppressions=${path.join(import.meta.dirname, "../../../leaksan.supp")}`, + }; + const cases = { + "generateKeyPair (KeyObject output)": `crypto.generateKeyPair("rsa", { modulusLength: 512 }, done);`, + "generateKeyPair (encrypted PEM output)": `crypto.generateKeyPair("rsa", { + modulusLength: 512, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase: "secret" }, + }, done);`, + "sign": `{ + const { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + crypto.sign("sha256", Buffer.from("data"), privateKey, done); + }`, + "diffieHellman": `{ + const a = crypto.generateKeyPairSync("x25519", {}); + const b = crypto.generateKeyPairSync("x25519", {}); + crypto.diffieHellman({ privateKey: a.privateKey, publicKey: b.publicKey }, done); + }`, + "hkdf": `crypto.hkdf("sha256", "key", "salt", "info", 32, done);`, + "checkPrime": `crypto.checkPrime(7n, done);`, + "generatePrime": `crypto.generatePrime(64, done);`, + "generateKey (secret)": `crypto.generateKey("hmac", { length: 256 }, done);`, + // The second path to the same leak: the completion task is enqueued but + // never dispatched (the spin keeps the JS thread busy until exit), so the + // shutdown release of queued jobs must free the ctx. + "generateKeyPair (exit before completion dispatch)": `{ + crypto.generateKeyPair("rsa", { modulusLength: 512 }, () => {}); + const end = Bun.nanoseconds() + 1_000_000_000; + while (Bun.nanoseconds() < end) {} + process.exit(0); + }`, + }; + + for (const [name, snippet] of Object.entries(cases)) { + test.concurrent(name, async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const crypto = require("crypto"); + const done = err => { + if (err) { console.error(err); process.exit(2); } + process.exit(0); + }; + ${snippet}`, + ], + env: lsanEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("LeakSanitizer"); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "", stderr: "", exitCode: 0 }); + }); + } +}); diff --git a/test/no-validate-leaksan.txt b/test/no-validate-leaksan.txt index daf59a57f3cf..6d9f6647cf64 100644 --- a/test/no-validate-leaksan.txt +++ b/test/no-validate-leaksan.txt @@ -446,6 +446,11 @@ test/js/bun/test/parallel/test-http-should-not-accept-untrusted-certificates.ts # Need to run the event loop once more to ensure sockets close test/js/node/test/parallel/test-https-localaddress-bind-error.js + +# process.exit() can race the work pool: a keygen finishing after the shutdown +# drain posts a completion task nothing can free (joining the pool at exit +# would block exit). Completions already queued at exit are released at +# shutdown; that path is covered in crypto.key-objects.test.ts. test/js/node/test/parallel/test-crypto-op-during-process-exit.js test/js/third_party/prisma/prisma.test.ts