From 2e1979cd01f320048bc04b942f4f12c3f7226877 Mon Sep 17 00:00:00 2001 From: Jordan Baczuk Date: Tue, 15 Sep 2026 09:42:08 -0600 Subject: [PATCH 1/3] Allow process.binding('async_wrap') for node-fibers fibers_sync.js saves/restores the async id stack across fiber switches via process.binding('async_wrap'). Upstream removed it from the allowlist; the hack then silently disables itself and the stack corrupts on the first yield. --- lib/internal/bootstrap/realm.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index 2ccceb493e68..1b47027fd63b 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -88,6 +88,10 @@ ObjectDefineProperty(process, 'moduleLoadList', { // more, we just implement them as legacy wrappers instead. See the // legacyWrapperList. const processBindingAllowList = new SafeSet([ + // Qualia: node-fibers (fibers_sync.js setupAsyncHacks) saves/restores the async id stack across + // fiber switches through process.binding('async_wrap'). Upstream dropped it from this list; without + // it the hack silently disables itself and the stack corrupts on the first yield. + 'async_wrap', 'buffer', 'cares_wrap', 'config', From 15804863d07771dd586a5a89736ab26660cbb5cb Mon Sep 17 00:00:00 2001 From: Jordan Baczuk Date: Tue, 15 Sep 2026 09:42:08 -0600 Subject: [PATCH 2/3] watch mode: tell the parent process the child is restarting Port of eeb46883d4 (znewsham/custom-v18). meteor-lite dev-run listens for 'watch:restarting'. IPC passthrough (#50890) and the watch:require / watch:import forwarding it relies on are upstream since v22. --- lib/internal/main/watch_mode.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index e864dfad1b84..0b3d792cdeb5 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -178,6 +178,11 @@ async function restart(child, trigger) { process.stdout.write(`${blue}Change detected in ${inspect(trigger)}${white}\n`); } process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`); + if (process.send) { + // A parent process (e.g. meteor-lite dev-run) uses this to know the child is going down + // before it starts seeing the new child's watch:require / watch:import messages. + process.send({ 'watch:restarting': {} }); + } await stop(child); return start(); } finally { From b2aebf478eec85ff77a363a9fbcc83b351b8cf7c Mon Sep 17 00:00:00 2001 From: Jordan Baczuk Date: Mon, 21 Sep 2026 08:50:52 -0600 Subject: [PATCH 3/3] crypto: keep the root cert store process-wide for thread-backed fibers Since v24 crypto_context.cc keeps root_cert_store, root_certs_from_users and has_cleanup_hook in static thread_local storage, so that each worker thread owns a root store which its Environment's cleanup hook frees. Qualia's Meteor services run on node-fibers with CORO_PTHREAD coroutines: the JavaScript of a single Environment executes on many OS threads, one per fiber. The first TLS handshake on a fiber thread finds an empty thread_local store, parses a fresh copy of the root certificates and registers the same (CleanupRootCertStore, nullptr) hook on the shared Environment a second time: Assertion failed: (insertion_info.second) == (true) node::CleanupQueue::Add <- SecureContext::SetCACert which killed global-deployment-center on its first HTTPS request. Even without the CHECK, every fiber thread would carry its own root store. Make the store process-wide again (as it was through v18) behind a mutex, register the cleanup hook once per Environment, and free the store when the last Environment that used it is torn down. NewRootCertStore() keeps its signature for the SecureContext and QUIC callers; the locked body moved to NewRootCertStoreLocked(). Co-Authored-By: Claude Fable 5.1 --- src/crypto/crypto_context.cc | 57 +++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index af8d52fbe699..eb07bba8e605 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -28,6 +28,7 @@ #endif #include +#include namespace node { @@ -92,16 +93,38 @@ struct X509Less { }; using X509Set = std::set; -// Per-thread root cert store. See NewRootCertStore() on what it contains. -static thread_local X509_STORE* root_cert_store = nullptr; +// Process-wide root cert store. See NewRootCertStore() on what it contains. +// +// Upstream keeps the store in thread_local storage (one per worker thread, +// freed by that thread's Environment cleanup hook). Qualia runs Meteor on +// node-fibers with thread-backed coroutines (CORO_PTHREAD), so the JavaScript +// of a single Environment executes on many OS threads, one per fiber. With +// thread_local storage every fiber thread that first performs a TLS handshake +// parses its own copy of the root store and registers the same +// (callback, argument) cleanup hook on the shared Environment again, which +// trips the CHECK in CleanupQueue::Add. The store is therefore process-wide, +// as it was through node 18, guarded by a mutex; the cleanup hook is +// registered once per Environment and the store is freed when the last +// Environment that used it is torn down. +static Mutex root_cert_store_mutex; +static X509_STORE* root_cert_store = nullptr; // If the user calls tls.setDefaultCACertificates() this will be used // to hold the user-provided certificates, the root_cert_store and any new // copy generated by NewRootCertStore() will then contain the certificates // from this set. -static thread_local std::unique_ptr root_certs_from_users; -static thread_local bool has_cleanup_hook = false; +static std::unique_ptr root_certs_from_users; +static std::unordered_set root_cert_store_envs; + +static X509_STORE* NewRootCertStoreLocked(Environment* env); + +static void CleanupRootCertStore(void* arg) { + Mutex::ScopedLock lock(root_cert_store_mutex); + root_cert_store_envs.erase(static_cast(arg)); + if (!root_cert_store_envs.empty()) { + // Another Environment still shares the store. + return; + } -static void CleanupRootCertStore(void*) { if (root_cert_store != nullptr) { X509_STORE_free(root_cert_store); root_cert_store = nullptr; @@ -113,25 +136,27 @@ static void CleanupRootCertStore(void*) { } root_certs_from_users.reset(); } - - has_cleanup_hook = false; } static void EnsureRootCertStoreCleanupHook(Environment* env) { - if (env == nullptr || has_cleanup_hook) { + if (env == nullptr) { return; } - env->AddCleanupHook(CleanupRootCertStore, nullptr); - has_cleanup_hook = true; + Mutex::ScopedLock lock(root_cert_store_mutex); + if (!root_cert_store_envs.insert(env).second) { + return; + } + env->AddCleanupHook(CleanupRootCertStore, env); } X509_STORE* GetOrCreateRootCertStore(Environment* env) { EnsureRootCertStoreCleanupHook(env); + Mutex::ScopedLock lock(root_cert_store_mutex); if (root_cert_store != nullptr) { return root_cert_store; } - root_cert_store = NewRootCertStore(env); + root_cert_store = NewRootCertStoreLocked(env); return root_cert_store; } @@ -1061,6 +1086,12 @@ void StartLoadingCertificatesOffThread( // the certificates provided by users. // TODO(joyeecheung): maybe these rules need a bit of consolidation? X509_STORE* NewRootCertStore(Environment* env) { + Mutex::ScopedLock lock(root_cert_store_mutex); + return NewRootCertStoreLocked(env); +} + +// Caller holds root_cert_store_mutex. +static X509_STORE* NewRootCertStoreLocked(Environment* env) { X509_STORE* store = X509_STORE_new(); CHECK_NOT_NULL(store); @@ -1246,6 +1277,7 @@ MaybeLocal X509sToArrayOfStrings(Environment* env, void GetUserRootCertificates(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + Mutex::ScopedLock lock(root_cert_store_mutex); CHECK_NOT_NULL(root_certs_from_users); Local results; if (X509sToArrayOfStrings(env, @@ -1266,6 +1298,7 @@ void ResetRootCertStore(const FunctionCallbackInfo& args) { if (cert_array->Length() == 0) { // If the array is empty, just clear the user certs and reset the store. + Mutex::ScopedLock lock(root_cert_store_mutex); if (root_cert_store != nullptr) { X509_STORE_free(root_cert_store); root_cert_store = nullptr; @@ -1303,6 +1336,8 @@ void ResetRootCertStore(const FunctionCallbackInfo& args) { } } + Mutex::ScopedLock lock(root_cert_store_mutex); + // Free any existing certificates in the old set. if (root_certs_from_users != nullptr) { for (X509* cert : *root_certs_from_users) {